ironflock 1.1.0__py3-none-any.whl → 1.2.1__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.
- ironflock/CrossbarConnection.py +343 -0
- ironflock/__init__.py +7 -7
- ironflock/ironflock.py +217 -104
- {ironflock-1.1.0.dist-info → ironflock-1.2.1.dist-info}/METADATA +18 -15
- ironflock-1.2.1.dist-info/RECORD +7 -0
- {ironflock-1.1.0.dist-info → ironflock-1.2.1.dist-info}/WHEEL +1 -2
- examples/app_session.py +0 -32
- examples/component.py +0 -40
- examples/simple_publish.py +0 -25
- examples/table_publish.py +0 -29
- ironflock/AutobahnConnection.py +0 -126
- ironflock-1.1.0.dist-info/RECORD +0 -13
- ironflock-1.1.0.dist-info/top_level.txt +0 -3
- test/main.py +0 -10
- {ironflock-1.1.0.dist-info → ironflock-1.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional, List, Dict, Any, Callable
|
|
5
|
+
from autobahn.asyncio.wamp import ApplicationSession
|
|
6
|
+
from autobahn.asyncio.component import Component
|
|
7
|
+
from autobahn.wamp import auth
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
class Stage(Enum):
|
|
11
|
+
"""Stage enumeration for different deployment environments"""
|
|
12
|
+
DEVELOPMENT = "DEV"
|
|
13
|
+
PRODUCTION = "PROD"
|
|
14
|
+
|
|
15
|
+
def getSerialNumber(serial_number: str = None) -> str:
|
|
16
|
+
"""Get device serial number from parameter or environment variable"""
|
|
17
|
+
if serial_number is None:
|
|
18
|
+
s_num = os.environ.get("DEVICE_SERIAL_NUMBER")
|
|
19
|
+
if s_num is None:
|
|
20
|
+
raise Exception("ENV Variable 'DEVICE_SERIAL_NUMBER' is not set!")
|
|
21
|
+
else:
|
|
22
|
+
s_num = serial_number
|
|
23
|
+
return s_num
|
|
24
|
+
|
|
25
|
+
DATAPODS_WS_URI = "wss://cbw.datapods.io/ws-ua-usr"
|
|
26
|
+
STUDIO_WS_URI_OLD = "wss://cbw.record-evolution.com/ws-ua-usr"
|
|
27
|
+
STUDIO_WS_URI = "wss://cbw.ironflock.com/ws-ua-usr"
|
|
28
|
+
LOCALHOST_WS_URI = "ws://localhost:8080/ws-ua-usr"
|
|
29
|
+
|
|
30
|
+
socketURIMap = {
|
|
31
|
+
"https://studio.datapods.io": DATAPODS_WS_URI,
|
|
32
|
+
"https://studio.record-evolution.com": STUDIO_WS_URI_OLD,
|
|
33
|
+
"https://studio.ironflock.com": STUDIO_WS_URI,
|
|
34
|
+
"http://localhost:8086": LOCALHOST_WS_URI,
|
|
35
|
+
"http://host.docker.internal:8086": LOCALHOST_WS_URI
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class CrossbarConnection:
|
|
39
|
+
"""
|
|
40
|
+
Python version of the TypeScript CrossbarConnection class.
|
|
41
|
+
Manages WAMP connections with automatic reconnection, subscription management, and session handling.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self.session: Optional[ApplicationSession] = None
|
|
46
|
+
self.component: Optional[Component] = None
|
|
47
|
+
self.connection_options: Optional[Dict[str, Any]] = None
|
|
48
|
+
self.subscriptions: List[Any] = [] # List of autobahn subscriptions
|
|
49
|
+
self.registrations: List[Any] = [] # List of autobahn registrations
|
|
50
|
+
self.connection_drop_wait_time: int = 6000 # milliseconds
|
|
51
|
+
self.realm: Optional[str] = None
|
|
52
|
+
self.serial_number: Optional[str] = None
|
|
53
|
+
self._first_connection_future: Optional[asyncio.Future] = None
|
|
54
|
+
self._is_connected = False
|
|
55
|
+
self._session_wait_timeout = 6.0 # seconds
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def getWebSocketURI():
|
|
59
|
+
reswarm_url = os.environ.get("RESWARM_URL")
|
|
60
|
+
if not reswarm_url:
|
|
61
|
+
return STUDIO_WS_URI
|
|
62
|
+
return socketURIMap.get(reswarm_url)
|
|
63
|
+
|
|
64
|
+
async def configure(
|
|
65
|
+
self,
|
|
66
|
+
swarm_key: int,
|
|
67
|
+
app_key: int,
|
|
68
|
+
stage: Stage,
|
|
69
|
+
cburl: Optional[str] = None,
|
|
70
|
+
serial_number: Optional[str] = None
|
|
71
|
+
):
|
|
72
|
+
"""
|
|
73
|
+
Configure the crossbar connection with realm and WAMPCRA authentication details.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
swarm_key: The swarm key identifier
|
|
77
|
+
app_key: The application key identifier
|
|
78
|
+
stage: The deployment stage (development, production)
|
|
79
|
+
cburl: Optional crossbar URL (defaults to environment variable RESWARM_URL mapping)
|
|
80
|
+
serial_number: Optional device serial number (defaults to DEVICE_SERIAL_NUMBER env var)
|
|
81
|
+
"""
|
|
82
|
+
self.realm = f"realm-{swarm_key}-{app_key}-{stage.value.lower()}"
|
|
83
|
+
self.serial_number = getSerialNumber(serial_number)
|
|
84
|
+
|
|
85
|
+
# Get URL from environment if not provided
|
|
86
|
+
if cburl is None:
|
|
87
|
+
cburl = self.getWebSocketURI()
|
|
88
|
+
|
|
89
|
+
self.connection_options = {
|
|
90
|
+
'url': cburl,
|
|
91
|
+
'realm': self.realm,
|
|
92
|
+
'authmethods': ['wampcra'],
|
|
93
|
+
'authid': self.serial_number,
|
|
94
|
+
'max_retries': -1, # infinite retries
|
|
95
|
+
'max_retry_delay': 2,
|
|
96
|
+
'initial_retry_delay': 1,
|
|
97
|
+
'serializers': ['msgpack']
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Create the component with WAMPCRA authentication
|
|
101
|
+
transports = [{
|
|
102
|
+
'type': 'websocket',
|
|
103
|
+
'url': cburl,
|
|
104
|
+
'serializers': ['msgpack']
|
|
105
|
+
}]
|
|
106
|
+
|
|
107
|
+
# Custom session class for WAMPCRA authentication
|
|
108
|
+
class AppSession(ApplicationSession):
|
|
109
|
+
def onConnect(session_self):
|
|
110
|
+
print('onConnect called')
|
|
111
|
+
session_self.join(self.realm, ['wampcra'], self.serial_number)
|
|
112
|
+
|
|
113
|
+
def onChallenge(session_self, challenge):
|
|
114
|
+
print(f'challenge requested for {challenge.method}')
|
|
115
|
+
if challenge.method == "wampcra":
|
|
116
|
+
signature = auth.compute_wcs(
|
|
117
|
+
self.serial_number, challenge.extra["challenge"]
|
|
118
|
+
)
|
|
119
|
+
return signature
|
|
120
|
+
raise Exception(f"Invalid authmethod {challenge.method}")
|
|
121
|
+
|
|
122
|
+
self.component = Component(
|
|
123
|
+
transports=transports,
|
|
124
|
+
realm=self.realm,
|
|
125
|
+
session_factory=AppSession
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Set up event handlers
|
|
129
|
+
self.component.on('join', self._on_open)
|
|
130
|
+
self.component.on('leave', self._on_close)
|
|
131
|
+
self.component.on('disconnect', self._on_disconnect)
|
|
132
|
+
|
|
133
|
+
# Create future for first connection
|
|
134
|
+
self._first_connection_future = asyncio.Future()
|
|
135
|
+
|
|
136
|
+
async def _on_open(self, session: ApplicationSession, details):
|
|
137
|
+
"""Handle session open event"""
|
|
138
|
+
self.session = session
|
|
139
|
+
self._is_connected = True
|
|
140
|
+
await self._resubscribe_all()
|
|
141
|
+
print(f"Connection to IronFlock app realm '{session._realm}' established")
|
|
142
|
+
|
|
143
|
+
if self._first_connection_future and not self._first_connection_future.done():
|
|
144
|
+
self._first_connection_future.set_result(None)
|
|
145
|
+
|
|
146
|
+
async def _on_close(self, session: ApplicationSession, details):
|
|
147
|
+
"""Handle session close event"""
|
|
148
|
+
self.session = None
|
|
149
|
+
self._is_connected = False
|
|
150
|
+
print(f"Connection to IronFlock app realm {self.realm} closed: {details.reason}")
|
|
151
|
+
|
|
152
|
+
if self._first_connection_future and not self._first_connection_future.done():
|
|
153
|
+
self._first_connection_future.set_exception(
|
|
154
|
+
Exception(f"Connection closed: {details.reason}")
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
async def _on_disconnect(self, session: ApplicationSession, was_clean: bool):
|
|
158
|
+
"""Handle disconnect event"""
|
|
159
|
+
self.session = None
|
|
160
|
+
self._is_connected = False
|
|
161
|
+
print(f"Disconnected from IronFlock app realm {self.realm}, clean: {was_clean}")
|
|
162
|
+
|
|
163
|
+
async def _session_wait(self) -> None:
|
|
164
|
+
"""Wait for session to be available"""
|
|
165
|
+
start_time = time.time()
|
|
166
|
+
while not self.session or not self._is_connected:
|
|
167
|
+
if time.time() - start_time > self._session_wait_timeout:
|
|
168
|
+
raise TimeoutError('Timeout waiting for session')
|
|
169
|
+
await asyncio.sleep(0.2)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def is_open(self) -> bool:
|
|
173
|
+
"""Check if the connection is open"""
|
|
174
|
+
return self._is_connected and self.session is not None
|
|
175
|
+
|
|
176
|
+
async def start(self) -> None:
|
|
177
|
+
"""Start the connection"""
|
|
178
|
+
if not self.component:
|
|
179
|
+
raise ValueError("Must call configure() before start()")
|
|
180
|
+
|
|
181
|
+
print(f'Starting connection for IronFlock app realm {self.realm}')
|
|
182
|
+
|
|
183
|
+
# Start the component (non-blocking in autobahn asyncio)
|
|
184
|
+
self.component.start()
|
|
185
|
+
|
|
186
|
+
# Wait for first connection to be established
|
|
187
|
+
if self._first_connection_future:
|
|
188
|
+
await self._first_connection_future
|
|
189
|
+
|
|
190
|
+
async def stop(self) -> None:
|
|
191
|
+
"""Stop the connection"""
|
|
192
|
+
if self.component:
|
|
193
|
+
await self.component.stop()
|
|
194
|
+
self._is_connected = False
|
|
195
|
+
self.session = None
|
|
196
|
+
|
|
197
|
+
async def call(
|
|
198
|
+
self,
|
|
199
|
+
topic: str,
|
|
200
|
+
args: Optional[List[Any]] = None,
|
|
201
|
+
kwargs: Optional[Dict[str, Any]] = None,
|
|
202
|
+
options: Optional[Dict[str, Any]] = None
|
|
203
|
+
) -> Any:
|
|
204
|
+
"""Call a remote procedure"""
|
|
205
|
+
await self._session_wait()
|
|
206
|
+
if not self.session:
|
|
207
|
+
raise RuntimeError("No active session")
|
|
208
|
+
|
|
209
|
+
args = args or []
|
|
210
|
+
kwargs = kwargs or {}
|
|
211
|
+
|
|
212
|
+
result = await self.session.call(topic, *args, **kwargs, options=options)
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
async def subscribe(
|
|
216
|
+
self,
|
|
217
|
+
topic: str,
|
|
218
|
+
handler: Callable,
|
|
219
|
+
options: Optional[Dict[str, Any]] = None
|
|
220
|
+
) -> Optional[Any]:
|
|
221
|
+
"""Subscribe to a topic"""
|
|
222
|
+
await self._session_wait()
|
|
223
|
+
if not self.session:
|
|
224
|
+
raise RuntimeError("No active session")
|
|
225
|
+
|
|
226
|
+
subscription = await self.session.subscribe(handler, topic, options=options)
|
|
227
|
+
if subscription:
|
|
228
|
+
self.subscriptions.append(subscription)
|
|
229
|
+
return subscription
|
|
230
|
+
|
|
231
|
+
async def publish(
|
|
232
|
+
self,
|
|
233
|
+
topic: str,
|
|
234
|
+
args: Optional[List[Any]] = None,
|
|
235
|
+
kwargs: Optional[Dict[str, Any]] = None,
|
|
236
|
+
options: Optional[Dict[str, Any]] = None
|
|
237
|
+
) -> Optional[Any]:
|
|
238
|
+
"""Publish to a topic"""
|
|
239
|
+
await self._session_wait()
|
|
240
|
+
if not self.session:
|
|
241
|
+
raise RuntimeError("No active session")
|
|
242
|
+
|
|
243
|
+
args = args or []
|
|
244
|
+
kwargs = kwargs or {}
|
|
245
|
+
|
|
246
|
+
result = await self.session.publish(topic, *args, options=options, **kwargs)
|
|
247
|
+
return result
|
|
248
|
+
|
|
249
|
+
async def unsubscribe(self, subscription: Any) -> None:
|
|
250
|
+
"""Unsubscribe from a subscription"""
|
|
251
|
+
await self._session_wait()
|
|
252
|
+
if not self.session:
|
|
253
|
+
raise RuntimeError("No active session")
|
|
254
|
+
|
|
255
|
+
if subscription in self.subscriptions:
|
|
256
|
+
self.subscriptions.remove(subscription)
|
|
257
|
+
await self.session.unsubscribe(subscription)
|
|
258
|
+
|
|
259
|
+
async def unsubscribe_func(
|
|
260
|
+
self,
|
|
261
|
+
topic: str,
|
|
262
|
+
handler: Optional[Callable] = None
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Unsubscribe a specific function from a topic"""
|
|
265
|
+
await self._session_wait()
|
|
266
|
+
|
|
267
|
+
if handler is None:
|
|
268
|
+
return await self.unsubscribe_topic(topic)
|
|
269
|
+
|
|
270
|
+
# Find and remove subscriptions matching topic and handler
|
|
271
|
+
to_remove = []
|
|
272
|
+
for subscription in self.subscriptions:
|
|
273
|
+
if hasattr(subscription, 'topic') and subscription.topic == topic:
|
|
274
|
+
if hasattr(subscription, 'handler') and subscription.handler == handler:
|
|
275
|
+
to_remove.append(subscription)
|
|
276
|
+
|
|
277
|
+
for subscription in to_remove:
|
|
278
|
+
try:
|
|
279
|
+
await self.unsubscribe(subscription)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
print(f"Failed to unsubscribe from {topic}: {e}")
|
|
282
|
+
|
|
283
|
+
async def unsubscribe_topic(self, topic: str) -> bool:
|
|
284
|
+
"""Unsubscribe all handlers from a topic"""
|
|
285
|
+
matching_subs = [
|
|
286
|
+
sub for sub in self.subscriptions
|
|
287
|
+
if hasattr(sub, 'topic') and sub.topic == topic
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
if not matching_subs:
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
# Unsubscribe all matching subscriptions
|
|
294
|
+
tasks = [self.unsubscribe(sub) for sub in matching_subs]
|
|
295
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
296
|
+
return True
|
|
297
|
+
|
|
298
|
+
async def register(
|
|
299
|
+
self,
|
|
300
|
+
topic: str,
|
|
301
|
+
endpoint: Callable,
|
|
302
|
+
options: Optional[Dict[str, Any]] = None
|
|
303
|
+
) -> Optional[Any]:
|
|
304
|
+
"""Register a procedure"""
|
|
305
|
+
await self._session_wait()
|
|
306
|
+
if not self.session:
|
|
307
|
+
raise RuntimeError("No active session")
|
|
308
|
+
|
|
309
|
+
registration = await self.session.register(endpoint, topic, options=options)
|
|
310
|
+
if registration:
|
|
311
|
+
self.registrations.append(registration)
|
|
312
|
+
return registration
|
|
313
|
+
|
|
314
|
+
async def unregister(self, registration: Any) -> None:
|
|
315
|
+
"""Unregister a procedure"""
|
|
316
|
+
await self._session_wait()
|
|
317
|
+
if not self.session:
|
|
318
|
+
raise RuntimeError("No active session")
|
|
319
|
+
|
|
320
|
+
if registration in self.registrations:
|
|
321
|
+
self.registrations.remove(registration)
|
|
322
|
+
await self.session.unregister(registration)
|
|
323
|
+
|
|
324
|
+
async def _resubscribe_all(self) -> None:
|
|
325
|
+
"""Resubscribe to all topics after reconnection"""
|
|
326
|
+
if not self.subscriptions:
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
print(f'Resubscribing to {len(self.subscriptions)} subscriptions')
|
|
330
|
+
|
|
331
|
+
# Store current subscriptions and clear the list
|
|
332
|
+
old_subscriptions = self.subscriptions.copy()
|
|
333
|
+
self.subscriptions.clear()
|
|
334
|
+
|
|
335
|
+
# Resubscribe to all topics
|
|
336
|
+
tasks = []
|
|
337
|
+
for old_sub in old_subscriptions:
|
|
338
|
+
if hasattr(old_sub, 'topic') and hasattr(old_sub, 'handler'):
|
|
339
|
+
task = self.subscribe(old_sub.topic, old_sub.handler)
|
|
340
|
+
tasks.append(task)
|
|
341
|
+
|
|
342
|
+
if tasks:
|
|
343
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
ironflock/__init__.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from ironflock.AutobahnConnection import (
|
|
2
|
-
create_application_component,
|
|
3
|
-
create_application_session,
|
|
4
|
-
)
|
|
5
|
-
|
|
6
1
|
from ironflock.ironflock import IronFlock
|
|
2
|
+
from ironflock.CrossbarConnection import CrossbarConnection, Stage
|
|
7
3
|
|
|
8
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"IronFlock",
|
|
6
|
+
"CrossbarConnection",
|
|
7
|
+
"Stage"
|
|
8
|
+
]
|
|
9
9
|
|
|
10
|
-
__version__ = "1.0
|
|
10
|
+
__version__ = "1.2.0"
|
ironflock/ironflock.py
CHANGED
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import asyncio
|
|
3
|
-
from typing import Optional
|
|
4
|
-
from autobahn.asyncio.component import Component, run
|
|
5
|
-
from autobahn.wamp.interfaces import ISession
|
|
6
|
-
from autobahn.wamp.types import PublishOptions, RegisterOptions
|
|
7
|
-
from autobahn.wamp.request import Publication
|
|
3
|
+
from typing import Optional, Any
|
|
8
4
|
|
|
9
|
-
from ironflock.
|
|
5
|
+
from ironflock.CrossbarConnection import CrossbarConnection, Stage, getSerialNumber
|
|
6
|
+
from autobahn.wamp.types import PublishOptions, RegisterOptions, SubscribeOptions, CallOptions
|
|
10
7
|
|
|
11
8
|
|
|
12
9
|
class IronFlock:
|
|
13
|
-
"""
|
|
10
|
+
"""Convenience class for easy-to-use message publishing in the IronFlock platform.
|
|
14
11
|
|
|
15
12
|
Example:
|
|
16
13
|
|
|
17
14
|
async def main():
|
|
18
15
|
while True:
|
|
19
|
-
publication = await
|
|
16
|
+
publication = await ironflock.publish("test.publish.pw", 1, "two", 3, foo="bar")
|
|
20
17
|
print(publication)
|
|
21
18
|
await asyncio.sleep(3)
|
|
22
19
|
|
|
23
20
|
|
|
24
21
|
if __name__ == "__main__":
|
|
25
22
|
ironflock = IronFlock(mainFunc=main)
|
|
26
|
-
|
|
23
|
+
await ironflock.run()
|
|
27
24
|
"""
|
|
28
25
|
|
|
29
26
|
def __init__(self, serial_number: str = None, mainFunc=None) -> None:
|
|
@@ -32,84 +29,114 @@ class IronFlock:
|
|
|
32
29
|
Args:
|
|
33
30
|
serial_number (str, optional): serial_number of device.
|
|
34
31
|
Defaults to None, in which case the environment variable DEVICE_SERIAL_NUMBER is used.
|
|
32
|
+
mainFunc (callable, optional): Main function to run after connection is established.
|
|
35
33
|
"""
|
|
36
34
|
self._serial_number = getSerialNumber(serial_number)
|
|
37
35
|
self._device_name = os.environ.get("DEVICE_NAME")
|
|
38
36
|
self._device_key = os.environ.get("DEVICE_KEY")
|
|
39
|
-
self.
|
|
40
|
-
self._session: ISession = None
|
|
37
|
+
self._connection = CrossbarConnection()
|
|
41
38
|
self.mainFunc = mainFunc
|
|
42
39
|
self._main_task = None
|
|
43
|
-
|
|
44
|
-
@self._component.on_join
|
|
45
|
-
async def onJoin(session, details):
|
|
46
|
-
print("component joined")
|
|
47
|
-
self._session = session
|
|
48
|
-
if self.mainFunc:
|
|
49
|
-
self._main_task = asyncio.create_task(mainFunc())
|
|
50
|
-
|
|
51
|
-
@self._component.on_disconnect
|
|
52
|
-
@self._component.on_leave
|
|
53
|
-
async def onLeave(*args, **kwargs):
|
|
54
|
-
print("component left")
|
|
55
|
-
if self._main_task:
|
|
56
|
-
self._main_task.cancel()
|
|
57
|
-
try:
|
|
58
|
-
await self._main_task
|
|
59
|
-
except asyncio.CancelledError:
|
|
60
|
-
pass
|
|
61
|
-
self._main_task = None
|
|
62
|
-
self._session = None
|
|
40
|
+
self._is_configured = False
|
|
63
41
|
|
|
64
42
|
@property
|
|
65
|
-
def
|
|
66
|
-
"""The
|
|
43
|
+
def connection(self) -> CrossbarConnection:
|
|
44
|
+
"""The CrossbarConnection instance
|
|
67
45
|
|
|
68
46
|
Returns:
|
|
69
|
-
|
|
47
|
+
CrossbarConnection
|
|
70
48
|
"""
|
|
71
|
-
return self.
|
|
49
|
+
return self._connection
|
|
72
50
|
|
|
73
51
|
@property
|
|
74
|
-
def
|
|
75
|
-
"""
|
|
52
|
+
def is_connected(self) -> bool:
|
|
53
|
+
"""Check if the connection is established
|
|
76
54
|
|
|
77
55
|
Returns:
|
|
78
|
-
|
|
56
|
+
bool: True if connected, False otherwise
|
|
79
57
|
"""
|
|
80
|
-
return self.
|
|
58
|
+
return self._connection.is_open
|
|
81
59
|
|
|
82
|
-
async def
|
|
60
|
+
async def _configure_connection(self):
|
|
61
|
+
"""Configure the CrossbarConnection with environment variables"""
|
|
62
|
+
if self._is_configured:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
swarm_key = int(os.environ.get("SWARM_KEY", 0))
|
|
66
|
+
app_key = int(os.environ.get("APP_KEY", 0))
|
|
67
|
+
env_value = os.environ.get("ENV", "DEV").upper()
|
|
68
|
+
|
|
69
|
+
# Map environment string to Stage enum
|
|
70
|
+
stage_map = {
|
|
71
|
+
"DEV": Stage.DEVELOPMENT,
|
|
72
|
+
"PROD": Stage.PRODUCTION
|
|
73
|
+
}
|
|
74
|
+
stage = stage_map.get(env_value, Stage.DEVELOPMENT)
|
|
75
|
+
|
|
76
|
+
await self._connection.configure(
|
|
77
|
+
swarm_key=swarm_key,
|
|
78
|
+
app_key=app_key,
|
|
79
|
+
stage=stage,
|
|
80
|
+
serial_number=self._serial_number
|
|
81
|
+
)
|
|
82
|
+
self._is_configured = True
|
|
83
|
+
|
|
84
|
+
async def publish(self, topic: str, *args, **kwargs) -> Optional[Any]:
|
|
83
85
|
"""Publishes to the IronFlock Platform Message Router
|
|
84
86
|
|
|
85
87
|
Args:
|
|
86
88
|
topic (str): The URI of the topic to publish to, e.g. "com.myapp.mytopic1"
|
|
89
|
+
*args: Positional arguments to publish
|
|
90
|
+
**kwargs: Keyword arguments to publish
|
|
87
91
|
|
|
88
92
|
Returns:
|
|
89
|
-
Optional[
|
|
93
|
+
Optional[Any]: Object representing a publication
|
|
90
94
|
(feedback from publishing an event when doing an acknowledged publish)
|
|
91
95
|
"""
|
|
96
|
+
if not self.is_connected:
|
|
97
|
+
print("cannot publish, not connected")
|
|
98
|
+
return None
|
|
92
99
|
|
|
93
|
-
|
|
100
|
+
# Add device metadata to kwargs
|
|
101
|
+
device_metadata = {
|
|
94
102
|
"DEVICE_SERIAL_NUMBER": self._serial_number,
|
|
95
103
|
"DEVICE_KEY": self._device_key,
|
|
96
104
|
"DEVICE_NAME": self._device_name,
|
|
97
|
-
"options": PublishOptions(acknowledge=True),
|
|
98
105
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
|
|
107
|
+
# Merge device metadata with user kwargs
|
|
108
|
+
combined_kwargs = {**device_metadata, **kwargs}
|
|
109
|
+
|
|
110
|
+
# Use acknowledged publish with proper PublishOptions
|
|
111
|
+
options = PublishOptions(acknowledge=True)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
pub = await self._connection.publish(
|
|
115
|
+
topic,
|
|
116
|
+
args=list(args),
|
|
117
|
+
kwargs=combined_kwargs,
|
|
118
|
+
options=options
|
|
119
|
+
)
|
|
102
120
|
return pub
|
|
103
|
-
|
|
104
|
-
print("
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"Publish failed: {e}")
|
|
123
|
+
return None
|
|
105
124
|
|
|
106
125
|
async def set_device_location(self, long: float, lat: float):
|
|
107
126
|
"""Update the location of the device registered in the platform
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
|
|
128
|
+
This will update the device's location in the master data of the platform.
|
|
129
|
+
The maps in the device or group overviews will reflect the new device location in realtime.
|
|
130
|
+
The location history will not be stored in the platform.
|
|
131
|
+
If you need location history, then create a dedicated table for it.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
long (float): Longitude coordinate
|
|
135
|
+
lat (float): Latitude coordinate
|
|
112
136
|
"""
|
|
137
|
+
if not self.is_connected:
|
|
138
|
+
print("cannot set location, not connected")
|
|
139
|
+
return None
|
|
113
140
|
|
|
114
141
|
payload = {
|
|
115
142
|
"long": long,
|
|
@@ -122,69 +149,118 @@ class IronFlock:
|
|
|
122
149
|
"DEVICE_NAME": self._device_name
|
|
123
150
|
}
|
|
124
151
|
|
|
125
|
-
|
|
126
|
-
res = await self.
|
|
127
|
-
|
|
152
|
+
try:
|
|
153
|
+
res = await self._connection.call(
|
|
154
|
+
'ironflock.location_service.update',
|
|
155
|
+
args=[payload],
|
|
156
|
+
kwargs=extra
|
|
157
|
+
)
|
|
158
|
+
return res
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print(f"Set location failed: {e}")
|
|
161
|
+
return None
|
|
128
162
|
|
|
129
|
-
async def
|
|
130
|
-
"""Registers a function
|
|
131
|
-
|
|
163
|
+
async def register(self, topic: str, endpoint, options: Optional[dict] = None) -> Optional[Any]:
|
|
164
|
+
"""Registers a function with the IronFlock Platform Message Router
|
|
165
|
+
|
|
132
166
|
Args:
|
|
133
|
-
topic (str): The URI of the topic to register
|
|
134
|
-
|
|
167
|
+
topic (str): The URI of the topic to register, e.g. "com.myapp.myprocedure1"
|
|
168
|
+
endpoint: The function to register
|
|
169
|
+
options (dict, optional): Registration options
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Optional[Any]: Object representing a registration
|
|
135
173
|
"""
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
174
|
+
if not self.is_connected:
|
|
175
|
+
print("cannot register, not connected")
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
# Convert options dict to RegisterOptions if provided
|
|
179
|
+
register_options = RegisterOptions(**options) if options else None
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
reg = await self._connection.register(topic, endpoint, options=register_options)
|
|
183
|
+
return reg
|
|
184
|
+
except Exception as e:
|
|
185
|
+
print(f"Register failed: {e}")
|
|
186
|
+
return None
|
|
146
187
|
|
|
147
|
-
async def
|
|
148
|
-
"""
|
|
188
|
+
async def subscribe(self, topic: str, handler, options: Optional[dict] = None) -> Optional[Any]:
|
|
189
|
+
"""Subscribes to a topic on the IronFlock Platform Message Router
|
|
149
190
|
|
|
150
191
|
Args:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
kwargs (dict): The keyword arguments to pass to the procedure.
|
|
192
|
+
topic (str): The URI of the topic to subscribe to, e.g. "com.myapp.mytopic1"
|
|
193
|
+
handler: The function to call when a message is received
|
|
194
|
+
options (dict, optional): Subscription options
|
|
155
195
|
|
|
156
196
|
Returns:
|
|
157
|
-
|
|
197
|
+
Optional[Any]: Object representing a subscription
|
|
158
198
|
"""
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
199
|
+
if not self.is_connected:
|
|
200
|
+
print("cannot subscribe, not connected")
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
# Convert options dict to SubscribeOptions if provided
|
|
204
|
+
subscribe_options = SubscribeOptions(**options) if options else None
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
sub = await self._connection.subscribe(topic, handler, options=subscribe_options)
|
|
208
|
+
return sub
|
|
209
|
+
except Exception as e:
|
|
210
|
+
print(f"Subscribe failed: {e}")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
async def call(self, device_key: str, topic: str, args: list = None, kwargs: dict = None, options: Optional[dict] = None):
|
|
214
|
+
"""Calls a remote procedure registered by another IronFlock device
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
device_key (str): The device key of the target device
|
|
218
|
+
topic (str): The URI of the topic to call, e.g. "com.myapp.myprocedure1"
|
|
219
|
+
args (list, optional): Positional arguments for the call. Defaults to None.
|
|
220
|
+
kwargs (dict, optional): Keyword arguments for the call. Defaults to None.
|
|
221
|
+
options (dict, optional): Call options. Defaults to None.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Any: The result of the remote procedure call
|
|
225
|
+
"""
|
|
226
|
+
if not self.is_connected:
|
|
169
227
|
print("cannot call, not connected")
|
|
170
228
|
return None
|
|
171
229
|
|
|
230
|
+
args = args or []
|
|
231
|
+
kwargs = kwargs or {}
|
|
232
|
+
|
|
233
|
+
# Convert options dict to CallOptions if provided
|
|
234
|
+
call_options = CallOptions(**options) if options else None
|
|
235
|
+
|
|
236
|
+
call_topic = f"{device_key}.{topic}"
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
result = await self._connection.call(call_topic, args=args, kwargs=kwargs, options=call_options)
|
|
240
|
+
return result
|
|
241
|
+
except Exception as e:
|
|
242
|
+
print(f"Call failed: {e}")
|
|
243
|
+
return None
|
|
244
|
+
|
|
172
245
|
async def publish_to_table(
|
|
173
246
|
self, tablename: str, *args, **kwargs
|
|
174
|
-
) -> Optional[
|
|
175
|
-
"""Publishes Data to a Table in the IronFlock Platform. This is a
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
247
|
+
) -> Optional[Any]:
|
|
248
|
+
"""Publishes Data to a Table in the IronFlock Platform. This is a convenience function.
|
|
249
|
+
|
|
250
|
+
You can achieve the same results by simply publishing a payload to the topic
|
|
251
|
+
[SWARM_KEY].[APP_KEY].[your_table_name]
|
|
252
|
+
|
|
253
|
+
The SWARM_KEY and APP_KEY are provided as environment variables to the device container.
|
|
254
|
+
The also provided ENV variable holds either PROD or DEV to decide which topic to use, above.
|
|
255
|
+
This function automatically detects the environment and publishes to the correct table.
|
|
256
|
+
|
|
183
257
|
Args:
|
|
184
258
|
tablename (str): The table name of the table to publish to, e.g. "sensordata"
|
|
259
|
+
*args: Positional arguments to publish
|
|
260
|
+
**kwargs: Keyword arguments to publish
|
|
185
261
|
|
|
186
262
|
Returns:
|
|
187
|
-
Optional[
|
|
263
|
+
Optional[Any]: Object representing a publication
|
|
188
264
|
(feedback from publishing an event when doing an acknowledged publish)
|
|
189
265
|
"""
|
|
190
266
|
|
|
@@ -193,7 +269,6 @@ class IronFlock:
|
|
|
193
269
|
|
|
194
270
|
swarm_key = os.environ.get("SWARM_KEY")
|
|
195
271
|
app_key = os.environ.get("APP_KEY")
|
|
196
|
-
env_value = os.environ.get("ENV")
|
|
197
272
|
|
|
198
273
|
if swarm_key is None:
|
|
199
274
|
raise Exception("Environment variable SWARM_KEY not set!")
|
|
@@ -206,9 +281,47 @@ class IronFlock:
|
|
|
206
281
|
pub = await self.publish(topic, *args, **kwargs)
|
|
207
282
|
return pub
|
|
208
283
|
|
|
209
|
-
def
|
|
210
|
-
"""
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
284
|
+
async def start(self):
|
|
285
|
+
"""Start the connection and run the main function if provided"""
|
|
286
|
+
await self._configure_connection()
|
|
287
|
+
await self._connection.start()
|
|
288
|
+
|
|
289
|
+
if self.mainFunc:
|
|
290
|
+
self._main_task = asyncio.create_task(self.mainFunc())
|
|
291
|
+
|
|
292
|
+
async def stop(self):
|
|
293
|
+
"""Stop the connection and cancel the main task if running"""
|
|
294
|
+
if self._main_task and not self._main_task.done():
|
|
295
|
+
self._main_task.cancel()
|
|
296
|
+
try:
|
|
297
|
+
await self._main_task
|
|
298
|
+
except asyncio.CancelledError:
|
|
299
|
+
pass
|
|
300
|
+
self._main_task = None
|
|
301
|
+
|
|
302
|
+
if self._connection:
|
|
303
|
+
await self._connection.stop()
|
|
304
|
+
|
|
305
|
+
async def run(self):
|
|
306
|
+
"""Start the connection and keep it running"""
|
|
307
|
+
await self.start()
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
# Keep running until manually stopped
|
|
311
|
+
if self._main_task:
|
|
312
|
+
await self._main_task
|
|
313
|
+
else:
|
|
314
|
+
# If no main function, just wait indefinitely
|
|
315
|
+
while self.is_connected:
|
|
316
|
+
await asyncio.sleep(1)
|
|
317
|
+
except KeyboardInterrupt:
|
|
318
|
+
print("Shutting down...")
|
|
319
|
+
except Exception as e:
|
|
320
|
+
print(f"Exception in run(): {e}")
|
|
321
|
+
raise
|
|
322
|
+
finally:
|
|
323
|
+
await self.stop()
|
|
324
|
+
|
|
325
|
+
def run_sync(self):
|
|
326
|
+
"""Synchronous wrapper to run the IronFlock instance (for backward compatibility)"""
|
|
327
|
+
asyncio.run(self.run())
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ironflock
|
|
3
|
-
Version: 1.1
|
|
4
|
-
Summary:
|
|
5
|
-
Author-email: Record Evolution GmbH <marko.petzold@record-evolution.de>
|
|
3
|
+
Version: 1.2.1
|
|
4
|
+
Summary: IronFlock Python SDK for connecting to the IronFlock Platform
|
|
6
5
|
License-Expression: MIT
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Marko Petzold, IronFlock GmbH
|
|
8
|
+
Author-email: info@ironflock.com
|
|
9
|
+
Requires-Python: >=3.9
|
|
9
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
11
|
Classifier: Intended Audience :: Developers
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Requires-
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Provides-Extra: docs
|
|
14
|
+
Requires-Dist: autobahn[asyncio,serialization] (==24.4.2)
|
|
15
|
+
Requires-Dist: mypy ; extra == "dev"
|
|
16
|
+
Requires-Dist: pydantic ; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest ; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff ; extra == "dev"
|
|
19
|
+
Requires-Dist: sphinx (>=7) ; extra == "docs"
|
|
20
|
+
Requires-Dist: sphinx-autodoc-typehints (>=2.0) ; extra == "docs"
|
|
21
|
+
Requires-Dist: sphinx-book-theme (>=1.1.4) ; extra == "docs"
|
|
22
|
+
Project-URL: Homepage, https://github.com/RecordEvolution/ironflock-py
|
|
23
|
+
Project-URL: Repository, https://github.com/RecordEvolution/ironflock-py
|
|
18
24
|
Description-Content-Type: text/markdown
|
|
19
|
-
License-File: LICENSE
|
|
20
|
-
Requires-Dist: autobahn[asyncio,serialization]==24.4.2
|
|
21
|
-
Dynamic: license-file
|
|
22
25
|
|
|
23
26
|
# ironflock
|
|
24
27
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ironflock/CrossbarConnection.py,sha256=kWoWQ8TqZORq5XhuL3-c22yIFBzGMpRbCZpztKPMNyQ,12802
|
|
2
|
+
ironflock/__init__.py,sha256=5DreQfXO9MKAaEDxHm_L2fzCdbt9gRg3K8bV--ESoMo,203
|
|
3
|
+
ironflock/ironflock.py,sha256=3Qd4VvrxTn5nekRZhwIzj_XFbr3Mayct0XzCkYSwK-8,11674
|
|
4
|
+
ironflock-1.2.1.dist-info/METADATA,sha256=4ZU45mFIsw8wQ5mMiMRySmtjFwSG2jwmuXfhzr--_BM,3425
|
|
5
|
+
ironflock-1.2.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
6
|
+
ironflock-1.2.1.dist-info/licenses/LICENSE,sha256=GpUKjPB381nmkbBIdX74vxXhsNZaNpngTOciss39Pjk,1073
|
|
7
|
+
ironflock-1.2.1.dist-info/RECORD,,
|
examples/app_session.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
###
|
|
2
|
-
# Minimal example of creating an Autobahn asyncio ApplicationSession
|
|
3
|
-
# for connecting to the IronFlock Platform.
|
|
4
|
-
# Compared to the IronFlock() class, the ApplicationSession approach allows
|
|
5
|
-
# more control reacting to the lifecycle of the Session:
|
|
6
|
-
# you can register callback functions like on_join or on_leave.
|
|
7
|
-
# For more details checkout:
|
|
8
|
-
# https://autobahn.readthedocs.io/en/latest/wamp/programming.html#application-components
|
|
9
|
-
###
|
|
10
|
-
|
|
11
|
-
from asyncio import sleep
|
|
12
|
-
from autobahn.wamp.interfaces import ISession
|
|
13
|
-
from ironflock import create_application_session
|
|
14
|
-
|
|
15
|
-
# returns an Autobahn asyncio ApplicationSession and ApplicationRunner, for more information checkout:
|
|
16
|
-
# https://autobahn.readthedocs.io/en/latest/reference/autobahn.asyncio.html
|
|
17
|
-
AppSession, runner = create_application_session()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class Application(AppSession):
|
|
21
|
-
async def onJoin(session: ISession, details):
|
|
22
|
-
print("joined router")
|
|
23
|
-
print(session, details)
|
|
24
|
-
|
|
25
|
-
# publish an event every second
|
|
26
|
-
while True:
|
|
27
|
-
session.publish("test.publish.com", 1, "two", 3, foo="bar")
|
|
28
|
-
await sleep(1)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if __name__ == "__main__":
|
|
32
|
-
runner.run(Application)
|
examples/component.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
###
|
|
2
|
-
# Minimal example of creating an Autobahn asyncio Component
|
|
3
|
-
# for connecting to the IronFlock Platform.
|
|
4
|
-
# Compared to the IronFlock() class, the Component approach allows
|
|
5
|
-
# more control, e.g. reacting to the lifecycle of the component:
|
|
6
|
-
# you can register callback functions like on_join or on_leave.
|
|
7
|
-
# For more details checkout:
|
|
8
|
-
# https://autobahn.readthedocs.io/en/latest/wamp/programming.html#application-components
|
|
9
|
-
###
|
|
10
|
-
|
|
11
|
-
from asyncio import sleep
|
|
12
|
-
from autobahn.asyncio.component import run
|
|
13
|
-
from autobahn.wamp.interfaces import ISession
|
|
14
|
-
from ironflock import create_application_component
|
|
15
|
-
|
|
16
|
-
# returns an Autobahn asyncio Component, for more information checkout:
|
|
17
|
-
# https://autobahn.readthedocs.io/en/latest/reference/autobahn.asyncio.html
|
|
18
|
-
comp = create_application_component()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@comp.on_join
|
|
22
|
-
async def onJoin(session: ISession, details):
|
|
23
|
-
print("joined router")
|
|
24
|
-
print(session, details)
|
|
25
|
-
|
|
26
|
-
def handler(*args, **kwargs):
|
|
27
|
-
print("got event")
|
|
28
|
-
print(args, kwargs)
|
|
29
|
-
|
|
30
|
-
# subscribe to a topic
|
|
31
|
-
session.subscribe(handler, "test.publish.com")
|
|
32
|
-
|
|
33
|
-
# publish an event every second
|
|
34
|
-
while True:
|
|
35
|
-
session.publish("test.publish.com", 1, "two", 3, foo="bar")
|
|
36
|
-
await sleep(1)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if __name__ == "__main__":
|
|
40
|
-
run([comp])
|
examples/simple_publish.py
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
###
|
|
2
|
-
# Minimal example of publishing events to the IronFlock Platform.
|
|
3
|
-
###
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
from ironflock import IronFlock
|
|
7
|
-
|
|
8
|
-
# create a ironflock instance, which auto connects to the IronFlock Platform
|
|
9
|
-
# the ironflock instance handles authentication and reconnects when connection is lost
|
|
10
|
-
rw = IronFlock()
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
async def main():
|
|
14
|
-
while True:
|
|
15
|
-
# publish an event (if connection is not established the publish is skipped)
|
|
16
|
-
publication = await rw.publish("test.publish.com", 1, "two", 3, foo="bar")
|
|
17
|
-
print(publication)
|
|
18
|
-
await asyncio.sleep(3)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if __name__ == "__main__":
|
|
22
|
-
# run the main coroutine
|
|
23
|
-
asyncio.get_event_loop().create_task(main())
|
|
24
|
-
# run the ironflock component
|
|
25
|
-
rw.run()
|
examples/table_publish.py
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
###
|
|
2
|
-
# Minimal example of publishing events to the IronFlock Platform.
|
|
3
|
-
###
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from ironflock import IronFlock
|
|
8
|
-
|
|
9
|
-
# create a ironflock instance, which auto connects to the IronFlock Platform
|
|
10
|
-
# the ironflock instance handles authentication and reconnects when connection is lost
|
|
11
|
-
rw = IronFlock()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
async def main():
|
|
15
|
-
while True:
|
|
16
|
-
# publish an event (if connection is not established the publish is skipped)
|
|
17
|
-
publication = await rw.publish_to_table(
|
|
18
|
-
"sensordata",
|
|
19
|
-
dict(temperature=25, tsp=datetime.now().astimezone().isoformat()),
|
|
20
|
-
)
|
|
21
|
-
print(publication)
|
|
22
|
-
await asyncio.sleep(3)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if __name__ == "__main__":
|
|
26
|
-
# run the main coroutine
|
|
27
|
-
asyncio.get_event_loop().create_task(main())
|
|
28
|
-
# run the ironflock component
|
|
29
|
-
rw.run()
|
ironflock/AutobahnConnection.py
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from typing import Tuple
|
|
3
|
-
from autobahn.asyncio.component import Component
|
|
4
|
-
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
|
|
5
|
-
from autobahn.wamp import auth
|
|
6
|
-
|
|
7
|
-
try:
|
|
8
|
-
SWARM_KEY = os.environ["SWARM_KEY"]
|
|
9
|
-
except:
|
|
10
|
-
raise Exception("Environment variable SWARM_KEY not set!")
|
|
11
|
-
|
|
12
|
-
try:
|
|
13
|
-
APP_KEY = os.environ["APP_KEY"]
|
|
14
|
-
except:
|
|
15
|
-
raise Exception("Environment variable APP_KEY not set!")
|
|
16
|
-
|
|
17
|
-
try:
|
|
18
|
-
ENV = os.environ["ENV"].lower()
|
|
19
|
-
except:
|
|
20
|
-
raise Exception("Environment variable ENV not set!")
|
|
21
|
-
|
|
22
|
-
# CB_REALM = "userapps"
|
|
23
|
-
CB_REALM = f"realm-{SWARM_KEY}-{APP_KEY}-{ENV}"
|
|
24
|
-
|
|
25
|
-
DATAPODS_WS_URI = "wss://cbw.datapods.io/ws-ua-usr"
|
|
26
|
-
STUDIO_WS_URI_OLD = "wss://cbw.record-evolution.com/ws-ua-usr"
|
|
27
|
-
STUDIO_WS_URI = "wss://cbw.ironflock.com/ws-ua-usr"
|
|
28
|
-
LOCALHOST_WS_URI = "ws://localhost:8080/ws-ua-usr"
|
|
29
|
-
|
|
30
|
-
socketURIMap = {
|
|
31
|
-
"https://studio.datapods.io": DATAPODS_WS_URI,
|
|
32
|
-
"https://studio.record-evolution.com": STUDIO_WS_URI_OLD,
|
|
33
|
-
"https://studio.ironflock.com": STUDIO_WS_URI,
|
|
34
|
-
"http://localhost:8086": LOCALHOST_WS_URI,
|
|
35
|
-
"http://host.docker.internal:8086": LOCALHOST_WS_URI
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def getWebSocketURI():
|
|
40
|
-
reswarm_url = os.environ.get("RESWARM_URL")
|
|
41
|
-
if not reswarm_url:
|
|
42
|
-
return STUDIO_WS_URI
|
|
43
|
-
return socketURIMap.get(reswarm_url)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def getSerialNumber(serial_number: str = None) -> str:
|
|
47
|
-
if serial_number is None:
|
|
48
|
-
s_num = os.environ.get("DEVICE_SERIAL_NUMBER")
|
|
49
|
-
if s_num is None:
|
|
50
|
-
raise Exception("ENV Variable 'DEVICE_SERIAL_NUMBER' is not set!")
|
|
51
|
-
else:
|
|
52
|
-
s_num = serial_number
|
|
53
|
-
return s_num
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class AppSession(ApplicationSession):
|
|
57
|
-
serial_number: str = None
|
|
58
|
-
|
|
59
|
-
def onConnect(self):
|
|
60
|
-
print('onConnect called')
|
|
61
|
-
if self.serial_number is None:
|
|
62
|
-
raise Exception("serial_number missing on AppSession")
|
|
63
|
-
|
|
64
|
-
self.join(CB_REALM, ["wampcra"], self.serial_number)
|
|
65
|
-
|
|
66
|
-
def onChallenge(self, challenge):
|
|
67
|
-
print('challenge requested for {}'.format(challenge.method))
|
|
68
|
-
if challenge.method == "wampcra":
|
|
69
|
-
if self.serial_number is None:
|
|
70
|
-
raise Exception("serial_number missing on AppSession")
|
|
71
|
-
|
|
72
|
-
signature = auth.compute_wcs(
|
|
73
|
-
self.serial_number, challenge.extra["challenge"]
|
|
74
|
-
)
|
|
75
|
-
return signature
|
|
76
|
-
|
|
77
|
-
raise Exception("Invalid authmethod {}".format(challenge.method))
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def create_application_session(
|
|
81
|
-
serial_number: str = None,
|
|
82
|
-
) -> Tuple[ApplicationSession, ApplicationRunner]:
|
|
83
|
-
"""Creates an Autobahn ApplicationSession and ApplicationRunner, which connects to the IronFlock Platform
|
|
84
|
-
|
|
85
|
-
Args:
|
|
86
|
-
serial_number (str, optional): serial_number of device.
|
|
87
|
-
Defaults to None, in which case the environment variable DEVICE_SERIAL_NUMBER is used.
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
Tuple[ApplicationSession, ApplicationRunner]
|
|
91
|
-
"""
|
|
92
|
-
AppSession.serial_number = getSerialNumber(serial_number)
|
|
93
|
-
|
|
94
|
-
runner = ApplicationRunner(
|
|
95
|
-
url=getWebSocketURI(),
|
|
96
|
-
realm=CB_REALM,
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
print('Application runner created')
|
|
100
|
-
|
|
101
|
-
return AppSession, runner
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def create_application_component(serial_number: str = None) -> Component:
|
|
105
|
-
"""Creates an Autobahn Component, which connects to the IronFlock Platform
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
serial_number (str, optional): serial_number of device.
|
|
109
|
-
Defaults to None, in which case the environment variable DEVICE_SERIAL_NUMBER is used.
|
|
110
|
-
|
|
111
|
-
Returns:
|
|
112
|
-
Component
|
|
113
|
-
"""
|
|
114
|
-
appSession, _ = create_application_session(serial_number)
|
|
115
|
-
|
|
116
|
-
comp = Component(
|
|
117
|
-
transports=[{
|
|
118
|
-
"url": getWebSocketURI(),
|
|
119
|
-
"serializers": ['msgpack'],
|
|
120
|
-
}],
|
|
121
|
-
realm=CB_REALM,
|
|
122
|
-
session_factory=appSession,
|
|
123
|
-
)
|
|
124
|
-
print('WAMP Component created')
|
|
125
|
-
|
|
126
|
-
return comp
|
ironflock-1.1.0.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
examples/app_session.py,sha256=ugpaZkqbhcn_DzIMrCLSoPjcPtacyKEDVWj0V2fHZJc,1151
|
|
2
|
-
examples/component.py,sha256=1wvYL1QSRh6t9csh_WPZiMB6GtfmU7PGwSYZtNfQyTg,1255
|
|
3
|
-
examples/simple_publish.py,sha256=3wImf1nikJEHynIvMSx4ofiSFhhbdsBJw3awLA-wYcE,720
|
|
4
|
-
examples/table_publish.py,sha256=IXsWRoLI9u1CqH_9mB-k4TLYEVOjGtcXzWQ3mzcxg1Q,831
|
|
5
|
-
ironflock/AutobahnConnection.py,sha256=Xh1Y7s269XdLTfDYe6moTiJda7cPzCkknICSqxN3fn4,3744
|
|
6
|
-
ironflock/__init__.py,sha256=Ia3Bb7BjyuGl1YBp3fHz0tHS-CsDtH-JSrjbtl-u7S4,264
|
|
7
|
-
ironflock/ironflock.py,sha256=3OftdBL1Fb4fMKNz3ttnycMAN3GmruIkFkjmKaOzahQ,7867
|
|
8
|
-
ironflock-1.1.0.dist-info/licenses/LICENSE,sha256=GpUKjPB381nmkbBIdX74vxXhsNZaNpngTOciss39Pjk,1073
|
|
9
|
-
test/main.py,sha256=Gp4N9nuUWrCDti4-9W5AoGFDX2cWujKg3Bbq1kJcEP8,328
|
|
10
|
-
ironflock-1.1.0.dist-info/METADATA,sha256=hD_x-g1q0ZV4BXPLIsYYTd5byJ2j1jGCKKxGXLPQBXc,3403
|
|
11
|
-
ironflock-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
ironflock-1.1.0.dist-info/top_level.txt,sha256=ik-lPYQ3yaZZm6kJURMiJmM2MJh9ExxRSOy-HruvpwY,24
|
|
13
|
-
ironflock-1.1.0.dist-info/RECORD,,
|
test/main.py
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
from asyncio.events import get_event_loop
|
|
2
|
-
from ironflock import IronFlock
|
|
3
|
-
|
|
4
|
-
async def main():
|
|
5
|
-
rw = IronFlock(serial_number="7652ee0b-c2cb-466a-b8ee-fec4167bf7ce")
|
|
6
|
-
result = await rw.publish('re.meetup.data', {"temperature": 20})
|
|
7
|
-
print(result)
|
|
8
|
-
|
|
9
|
-
if __name__ == "__main__":
|
|
10
|
-
get_event_loop().run_until_complete(main())
|
|
File without changes
|