ironflock 1.0.9__py3-none-any.whl → 1.2.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.
- ironflock/CrossbarConnection.py +343 -0
- ironflock/__init__.py +7 -7
- ironflock/ironflock.py +167 -82
- {ironflock-1.0.9.dist-info → ironflock-1.2.0.dist-info}/METADATA +49 -21
- ironflock-1.2.0.dist-info/RECORD +7 -0
- {ironflock-1.0.9.dist-info → ironflock-1.2.0.dist-info}/WHEEL +1 -2
- ironflock/AutobahnConnection.py +0 -126
- ironflock-1.0.9.dist-info/RECORD +0 -8
- ironflock-1.0.9.dist-info/top_level.txt +0 -1
- {ironflock-1.0.9.dist-info → ironflock-1.2.0.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 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 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 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 realm {self.realm}')
|
|
182
|
+
|
|
183
|
+
# Start the component
|
|
184
|
+
await self.component.start()
|
|
185
|
+
|
|
186
|
+
# Wait for first connection
|
|
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,25 @@
|
|
|
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
|
|
10
6
|
|
|
11
7
|
|
|
12
8
|
class IronFlock:
|
|
13
|
-
"""
|
|
9
|
+
"""Convenience class for easy-to-use message publishing in the IronFlock platform.
|
|
14
10
|
|
|
15
11
|
Example:
|
|
16
12
|
|
|
17
13
|
async def main():
|
|
18
14
|
while True:
|
|
19
|
-
publication = await
|
|
15
|
+
publication = await ironflock.publish("test.publish.pw", 1, "two", 3, foo="bar")
|
|
20
16
|
print(publication)
|
|
21
17
|
await asyncio.sleep(3)
|
|
22
18
|
|
|
23
19
|
|
|
24
20
|
if __name__ == "__main__":
|
|
25
21
|
ironflock = IronFlock(mainFunc=main)
|
|
26
|
-
|
|
22
|
+
await ironflock.run()
|
|
27
23
|
"""
|
|
28
24
|
|
|
29
25
|
def __init__(self, serial_number: str = None, mainFunc=None) -> None:
|
|
@@ -32,84 +28,114 @@ class IronFlock:
|
|
|
32
28
|
Args:
|
|
33
29
|
serial_number (str, optional): serial_number of device.
|
|
34
30
|
Defaults to None, in which case the environment variable DEVICE_SERIAL_NUMBER is used.
|
|
31
|
+
mainFunc (callable, optional): Main function to run after connection is established.
|
|
35
32
|
"""
|
|
36
33
|
self._serial_number = getSerialNumber(serial_number)
|
|
37
34
|
self._device_name = os.environ.get("DEVICE_NAME")
|
|
38
35
|
self._device_key = os.environ.get("DEVICE_KEY")
|
|
39
|
-
self.
|
|
40
|
-
self._session: ISession = None
|
|
36
|
+
self._connection = CrossbarConnection()
|
|
41
37
|
self.mainFunc = mainFunc
|
|
42
38
|
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
|
|
39
|
+
self._is_configured = False
|
|
63
40
|
|
|
64
41
|
@property
|
|
65
|
-
def
|
|
66
|
-
"""The
|
|
42
|
+
def connection(self) -> CrossbarConnection:
|
|
43
|
+
"""The CrossbarConnection instance
|
|
67
44
|
|
|
68
45
|
Returns:
|
|
69
|
-
|
|
46
|
+
CrossbarConnection
|
|
70
47
|
"""
|
|
71
|
-
return self.
|
|
48
|
+
return self._connection
|
|
72
49
|
|
|
73
50
|
@property
|
|
74
|
-
def
|
|
75
|
-
"""
|
|
51
|
+
def is_connected(self) -> bool:
|
|
52
|
+
"""Check if the connection is established
|
|
76
53
|
|
|
77
54
|
Returns:
|
|
78
|
-
|
|
55
|
+
bool: True if connected, False otherwise
|
|
79
56
|
"""
|
|
80
|
-
return self.
|
|
57
|
+
return self._connection.is_open
|
|
81
58
|
|
|
82
|
-
async def
|
|
59
|
+
async def _configure_connection(self):
|
|
60
|
+
"""Configure the CrossbarConnection with environment variables"""
|
|
61
|
+
if self._is_configured:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
swarm_key = int(os.environ.get("SWARM_KEY", 0))
|
|
65
|
+
app_key = int(os.environ.get("APP_KEY", 0))
|
|
66
|
+
env_value = os.environ.get("ENV", "DEV").upper()
|
|
67
|
+
|
|
68
|
+
# Map environment string to Stage enum
|
|
69
|
+
stage_map = {
|
|
70
|
+
"DEV": Stage.DEVELOPMENT,
|
|
71
|
+
"PROD": Stage.PRODUCTION
|
|
72
|
+
}
|
|
73
|
+
stage = stage_map.get(env_value, Stage.DEVELOPMENT)
|
|
74
|
+
|
|
75
|
+
await self._connection.configure(
|
|
76
|
+
swarm_key=swarm_key,
|
|
77
|
+
app_key=app_key,
|
|
78
|
+
stage=stage,
|
|
79
|
+
serial_number=self._serial_number
|
|
80
|
+
)
|
|
81
|
+
self._is_configured = True
|
|
82
|
+
|
|
83
|
+
async def publish(self, topic: str, *args, **kwargs) -> Optional[Any]:
|
|
83
84
|
"""Publishes to the IronFlock Platform Message Router
|
|
84
85
|
|
|
85
86
|
Args:
|
|
86
87
|
topic (str): The URI of the topic to publish to, e.g. "com.myapp.mytopic1"
|
|
88
|
+
*args: Positional arguments to publish
|
|
89
|
+
**kwargs: Keyword arguments to publish
|
|
87
90
|
|
|
88
91
|
Returns:
|
|
89
|
-
Optional[
|
|
92
|
+
Optional[Any]: Object representing a publication
|
|
90
93
|
(feedback from publishing an event when doing an acknowledged publish)
|
|
91
94
|
"""
|
|
95
|
+
if not self.is_connected:
|
|
96
|
+
print("cannot publish, not connected")
|
|
97
|
+
return None
|
|
92
98
|
|
|
93
|
-
|
|
99
|
+
# Add device metadata to kwargs
|
|
100
|
+
device_metadata = {
|
|
94
101
|
"DEVICE_SERIAL_NUMBER": self._serial_number,
|
|
95
102
|
"DEVICE_KEY": self._device_key,
|
|
96
103
|
"DEVICE_NAME": self._device_name,
|
|
97
|
-
"options": PublishOptions(acknowledge=True),
|
|
98
104
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
|
|
106
|
+
# Merge device metadata with user kwargs
|
|
107
|
+
combined_kwargs = {**device_metadata, **kwargs}
|
|
108
|
+
|
|
109
|
+
# Use acknowledged publish
|
|
110
|
+
options = {"acknowledge": True}
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
pub = await self._connection.publish(
|
|
114
|
+
topic,
|
|
115
|
+
args=list(args),
|
|
116
|
+
kwargs=combined_kwargs,
|
|
117
|
+
options=options
|
|
118
|
+
)
|
|
102
119
|
return pub
|
|
103
|
-
|
|
104
|
-
print("
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print(f"Publish failed: {e}")
|
|
122
|
+
return None
|
|
105
123
|
|
|
106
124
|
async def set_device_location(self, long: float, lat: float):
|
|
107
125
|
"""Update the location of the device registered in the platform
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
|
|
127
|
+
This will update the device's location in the master data of the platform.
|
|
128
|
+
The maps in the device or group overviews will reflect the new device location in realtime.
|
|
129
|
+
The location history will not be stored in the platform.
|
|
130
|
+
If you need location history, then create a dedicated table for it.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
long (float): Longitude coordinate
|
|
134
|
+
lat (float): Latitude coordinate
|
|
112
135
|
"""
|
|
136
|
+
if not self.is_connected:
|
|
137
|
+
print("cannot set location, not connected")
|
|
138
|
+
return None
|
|
113
139
|
|
|
114
140
|
payload = {
|
|
115
141
|
"long": long,
|
|
@@ -122,9 +148,16 @@ class IronFlock:
|
|
|
122
148
|
"DEVICE_NAME": self._device_name
|
|
123
149
|
}
|
|
124
150
|
|
|
125
|
-
|
|
126
|
-
res = await self.
|
|
127
|
-
|
|
151
|
+
try:
|
|
152
|
+
res = await self._connection.call(
|
|
153
|
+
'ironflock.location_service.update',
|
|
154
|
+
args=[payload],
|
|
155
|
+
kwargs=extra
|
|
156
|
+
)
|
|
157
|
+
return res
|
|
158
|
+
except Exception as e:
|
|
159
|
+
print(f"Set location failed: {e}")
|
|
160
|
+
return None
|
|
128
161
|
|
|
129
162
|
async def register_function(self, topic: str, func):
|
|
130
163
|
"""Registers a function to be called when a message is received on the given topic.
|
|
@@ -133,18 +166,26 @@ class IronFlock:
|
|
|
133
166
|
topic (str): The URI of the topic to register the function for, e.g. "example.mytopic1".
|
|
134
167
|
func (callable): The function to call when a message is received on the topic.
|
|
135
168
|
"""
|
|
169
|
+
if not self.is_connected:
|
|
170
|
+
print("cannot register function, not connected")
|
|
171
|
+
return None
|
|
172
|
+
|
|
136
173
|
swarm_key = os.environ.get("SWARM_KEY")
|
|
137
174
|
app_key = os.environ.get("APP_KEY")
|
|
138
175
|
env_value = os.environ.get("ENV")
|
|
139
176
|
|
|
140
177
|
full_topic = f"{swarm_key}.{self._device_key}.{app_key}.{env_value}.{topic}"
|
|
141
178
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
179
|
+
try:
|
|
180
|
+
# Note: CrossbarConnection doesn't support force_reregister option directly
|
|
181
|
+
# but it handles resubscription automatically on reconnect
|
|
182
|
+
registration = await self._connection.register(full_topic, func)
|
|
183
|
+
return registration
|
|
184
|
+
except Exception as e:
|
|
185
|
+
print(f"Register function failed: {e}")
|
|
186
|
+
return None
|
|
146
187
|
|
|
147
|
-
async def call(self, device_key, topic, args, kwargs):
|
|
188
|
+
async def call(self, device_key: str, topic: str, args: list = None, kwargs: dict = None):
|
|
148
189
|
"""Calls a remote procedure on the IronFlock platform.
|
|
149
190
|
|
|
150
191
|
Args:
|
|
@@ -156,35 +197,46 @@ class IronFlock:
|
|
|
156
197
|
Returns:
|
|
157
198
|
The result of the remote procedure call.
|
|
158
199
|
"""
|
|
159
|
-
|
|
200
|
+
if not self.is_connected:
|
|
201
|
+
print("cannot call, not connected")
|
|
202
|
+
return None
|
|
203
|
+
|
|
160
204
|
swarm_key = os.environ.get("SWARM_KEY")
|
|
161
205
|
app_key = os.environ.get("APP_KEY")
|
|
162
206
|
env_value = os.environ.get("ENV")
|
|
163
207
|
|
|
164
208
|
full_topic = f"{swarm_key}.{device_key}.{app_key}.{env_value}.{topic}"
|
|
165
209
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
210
|
+
try:
|
|
211
|
+
result = await self._connection.call(
|
|
212
|
+
full_topic,
|
|
213
|
+
args=args or [],
|
|
214
|
+
kwargs=kwargs or {}
|
|
215
|
+
)
|
|
216
|
+
return result
|
|
217
|
+
except Exception as e:
|
|
218
|
+
print(f"Call failed: {e}")
|
|
170
219
|
return None
|
|
171
220
|
|
|
172
221
|
async def publish_to_table(
|
|
173
222
|
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
|
-
|
|
223
|
+
) -> Optional[Any]:
|
|
224
|
+
"""Publishes Data to a Table in the IronFlock Platform. This is a convenience function.
|
|
225
|
+
|
|
226
|
+
You can achieve the same results by simply publishing a payload to the topic
|
|
227
|
+
[SWARM_KEY].[APP_KEY].[your_table_name]
|
|
228
|
+
|
|
229
|
+
The SWARM_KEY and APP_KEY are provided as environment variables to the device container.
|
|
230
|
+
The also provided ENV variable holds either PROD or DEV to decide which topic to use, above.
|
|
231
|
+
This function automatically detects the environment and publishes to the correct table.
|
|
232
|
+
|
|
183
233
|
Args:
|
|
184
234
|
tablename (str): The table name of the table to publish to, e.g. "sensordata"
|
|
235
|
+
*args: Positional arguments to publish
|
|
236
|
+
**kwargs: Keyword arguments to publish
|
|
185
237
|
|
|
186
238
|
Returns:
|
|
187
|
-
Optional[
|
|
239
|
+
Optional[Any]: Object representing a publication
|
|
188
240
|
(feedback from publishing an event when doing an acknowledged publish)
|
|
189
241
|
"""
|
|
190
242
|
|
|
@@ -193,7 +245,6 @@ class IronFlock:
|
|
|
193
245
|
|
|
194
246
|
swarm_key = os.environ.get("SWARM_KEY")
|
|
195
247
|
app_key = os.environ.get("APP_KEY")
|
|
196
|
-
env_value = os.environ.get("ENV")
|
|
197
248
|
|
|
198
249
|
if swarm_key is None:
|
|
199
250
|
raise Exception("Environment variable SWARM_KEY not set!")
|
|
@@ -206,9 +257,43 @@ class IronFlock:
|
|
|
206
257
|
pub = await self.publish(topic, *args, **kwargs)
|
|
207
258
|
return pub
|
|
208
259
|
|
|
209
|
-
def
|
|
210
|
-
"""
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
260
|
+
async def start(self):
|
|
261
|
+
"""Start the connection and run the main function if provided"""
|
|
262
|
+
await self._configure_connection()
|
|
263
|
+
await self._connection.start()
|
|
264
|
+
|
|
265
|
+
if self.mainFunc:
|
|
266
|
+
self._main_task = asyncio.create_task(self.mainFunc())
|
|
267
|
+
|
|
268
|
+
async def stop(self):
|
|
269
|
+
"""Stop the connection and cancel the main task if running"""
|
|
270
|
+
if self._main_task and not self._main_task.done():
|
|
271
|
+
self._main_task.cancel()
|
|
272
|
+
try:
|
|
273
|
+
await self._main_task
|
|
274
|
+
except asyncio.CancelledError:
|
|
275
|
+
pass
|
|
276
|
+
self._main_task = None
|
|
277
|
+
|
|
278
|
+
await self._connection.stop()
|
|
279
|
+
|
|
280
|
+
async def run(self):
|
|
281
|
+
"""Start the connection and keep it running"""
|
|
282
|
+
await self.start()
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
# Keep running until manually stopped
|
|
286
|
+
if self._main_task:
|
|
287
|
+
await self._main_task
|
|
288
|
+
else:
|
|
289
|
+
# If no main function, just wait indefinitely
|
|
290
|
+
while self.is_connected:
|
|
291
|
+
await asyncio.sleep(1)
|
|
292
|
+
except KeyboardInterrupt:
|
|
293
|
+
print("Shutting down...")
|
|
294
|
+
finally:
|
|
295
|
+
await self.stop()
|
|
296
|
+
|
|
297
|
+
def run_sync(self):
|
|
298
|
+
"""Synchronous wrapper to run the IronFlock instance (for backward compatibility)"""
|
|
299
|
+
asyncio.run(self.run())
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ironflock
|
|
3
|
-
Version: 1.0
|
|
4
|
-
Summary:
|
|
5
|
-
|
|
6
|
-
Author: Record Evolution GmbH
|
|
7
|
-
Author-email: marko.petzold@record-evolution.de
|
|
8
|
-
License: MIT
|
|
9
|
-
Requires-Python: >=3.8
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: IronFlock Python SDK for connecting to the IronFlock Platform
|
|
5
|
+
License-Expression: MIT
|
|
11
6
|
License-File: LICENSE
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
Author: Marko Petzold, IronFlock GmbH
|
|
8
|
+
Author-email: info@ironflock.com
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
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
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
23
25
|
|
|
24
26
|
# ironflock
|
|
25
27
|
|
|
@@ -32,6 +34,19 @@ of the device's fleet and the data is collected in the respective fleet database
|
|
|
32
34
|
So if you use the library in your app, the data collection will always be private to the app user's fleet.
|
|
33
35
|
|
|
34
36
|
For more information on the IronFlock IoT Devops Platform for engineers and developers visit our [IronFlock](https://www.ironflock.com) home page.
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- Python 3.8 or higher
|
|
41
|
+
- Compatible with Python 3.8, 3.9, 3.10, 3.11, and 3.12
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Install from PyPI:
|
|
46
|
+
|
|
47
|
+
```shell
|
|
48
|
+
pip install ironflock
|
|
49
|
+
```
|
|
35
50
|
## Usage
|
|
36
51
|
|
|
37
52
|
```python
|
|
@@ -75,10 +90,10 @@ the [examples](https://github.com/RecordEvolution/ironflock-py/tree/main/example
|
|
|
75
90
|
|
|
76
91
|
## Development
|
|
77
92
|
|
|
78
|
-
Install the necessary
|
|
93
|
+
Install the necessary build tools if you don't have them already:
|
|
79
94
|
|
|
80
95
|
```shell
|
|
81
|
-
pip install --upgrade
|
|
96
|
+
pip install --upgrade build twine
|
|
82
97
|
```
|
|
83
98
|
|
|
84
99
|
Build and publish a new pypi package:
|
|
@@ -87,4 +102,17 @@ Build and publish a new pypi package:
|
|
|
87
102
|
just publish
|
|
88
103
|
```
|
|
89
104
|
|
|
105
|
+
Alternatively, you can build manually:
|
|
106
|
+
|
|
107
|
+
```shell
|
|
108
|
+
# Clean previous builds
|
|
109
|
+
rm -rf build dist *.egg-info
|
|
110
|
+
|
|
111
|
+
# Build the package
|
|
112
|
+
python -m build
|
|
113
|
+
|
|
114
|
+
# Upload to PyPI
|
|
115
|
+
twine upload dist/*
|
|
116
|
+
```
|
|
117
|
+
|
|
90
118
|
Check the package at https://pypi.org/project/ironflock/.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ironflock/CrossbarConnection.py,sha256=Q7SNHTNbi7AJC4SbFf7_TLqS10dIGGzJnwY0iVIU2Ek,12727
|
|
2
|
+
ironflock/__init__.py,sha256=5DreQfXO9MKAaEDxHm_L2fzCdbt9gRg3K8bV--ESoMo,203
|
|
3
|
+
ironflock/ironflock.py,sha256=Efs6cAl7dP6CzN1XRFQRg6xw7ULIX_4EvXpXDXUJUK0,10531
|
|
4
|
+
ironflock-1.2.0.dist-info/METADATA,sha256=6N0-bI8AzKlbBTO4kMj_fQSdOHBVCXLczzaF_r6uoJI,3425
|
|
5
|
+
ironflock-1.2.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
6
|
+
ironflock-1.2.0.dist-info/licenses/LICENSE,sha256=GpUKjPB381nmkbBIdX74vxXhsNZaNpngTOciss39Pjk,1073
|
|
7
|
+
ironflock-1.2.0.dist-info/RECORD,,
|
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.0.9.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
ironflock/AutobahnConnection.py,sha256=Xh1Y7s269XdLTfDYe6moTiJda7cPzCkknICSqxN3fn4,3744
|
|
2
|
-
ironflock/__init__.py,sha256=Ia3Bb7BjyuGl1YBp3fHz0tHS-CsDtH-JSrjbtl-u7S4,264
|
|
3
|
-
ironflock/ironflock.py,sha256=3OftdBL1Fb4fMKNz3ttnycMAN3GmruIkFkjmKaOzahQ,7867
|
|
4
|
-
ironflock-1.0.9.dist-info/licenses/LICENSE,sha256=GpUKjPB381nmkbBIdX74vxXhsNZaNpngTOciss39Pjk,1073
|
|
5
|
-
ironflock-1.0.9.dist-info/METADATA,sha256=eEG8bSTbMgUJlf9hdMsGxkwlDDFr--j-icJAnBx-8PU,2764
|
|
6
|
-
ironflock-1.0.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
ironflock-1.0.9.dist-info/top_level.txt,sha256=hmMdMPJuvnOTlFKYl1XQOn81vg1DE2LT7xrEXgyxcRA,10
|
|
8
|
-
ironflock-1.0.9.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
ironflock
|
|
File without changes
|