xconn 0.1.0__tar.gz

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.
xconn-0.1.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 XConnIO
4
+ Copyright (c) 2024 Simple Things Inc.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
xconn-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.1
2
+ Name: xconn
3
+ Version: 0.1.0
4
+ Summary: https://github.com/xconnio/xconn-python
5
+ Author-email: Omer Akram <omer@thing.com>, Mahad Munir <mahad@xconn.io>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 XConnIO
9
+ Copyright (c) 2024 Simple Things Inc.
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Project-URL: Homepage, https://github.com/xconnio/xconn-python
30
+ Project-URL: Bug Tracker, https://github.com/xconnio/xconn-python/issues
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Programming Language :: Python :: 3.10
34
+ Classifier: Programming Language :: Python :: 3.11
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Requires-Python: >=3.10
37
+ Description-Content-Type: text/markdown
38
+ License-File: LICENSE
39
+ Requires-Dist: websockets
40
+ Requires-Dist: wampproto
41
+ Requires-Dist: aiohttp
42
+ Requires-Dist: uvloop
43
+ Provides-Extra: test
44
+ Requires-Dist: coverage; extra == "test"
45
+ Requires-Dist: pytest; extra == "test"
46
+ Requires-Dist: pytest-asyncio; extra == "test"
47
+ Requires-Dist: ruff; extra == "test"
48
+
49
+ # XConn Python
xconn-0.1.0/README.md ADDED
@@ -0,0 +1 @@
1
+ # XConn Python
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xconn"
7
+ version = "0.1.0"
8
+ authors = [
9
+ {name="Omer Akram", email="omer@thing.com"},
10
+ {name="Mahad Munir", email="mahad@xconn.io"},
11
+ ]
12
+ description = "https://github.com/xconnio/xconn-python"
13
+ readme = "README.md"
14
+ license = {file = "LICENSE"}
15
+ dependencies = [
16
+ "websockets",
17
+ "wampproto",
18
+ "aiohttp",
19
+ "uvloop",
20
+ ]
21
+ requires-python = ">=3.10"
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ test = [
32
+ "coverage",
33
+ "pytest",
34
+ "pytest-asyncio",
35
+ "ruff",
36
+ ]
37
+
38
+ [project.scripts]
39
+ "xconn" = "xconn.__main__:main"
40
+
41
+ [project.urls]
42
+ "Homepage" = "https://github.com/xconnio/xconn-python"
43
+ "Bug Tracker" = "https://github.com/xconnio/xconn-python/issues"
xconn-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,68 @@
1
+ import asyncio
2
+ import argparse
3
+ import importlib
4
+ import sys
5
+
6
+ import uvloop
7
+ from wampproto.serializers import CBORSerializer
8
+
9
+ from xconn.app import WampApp
10
+ from xconn.router import Router
11
+ from xconn.server import Server
12
+ from xconn.session import AsyncSession
13
+ from xconn.types import ServerSideLocalBaseSession, ClientSideLocalBaseSession
14
+
15
+
16
+ def main():
17
+ parser = argparse.ArgumentParser()
18
+ parser.add_argument("--port", type=int, default=8080)
19
+ parser.add_argument("--host", type=str, default="127.0.0.1")
20
+ parser.add_argument("--realm", type=str, default="realm1")
21
+ parser.add_argument("--directory", type=str, default=".")
22
+ parser.add_argument("APP")
23
+
24
+ parsed = parser.parse_args()
25
+
26
+ split = parsed.APP.split(":")
27
+ if len(split) != 2:
28
+ raise RuntimeError("invalid app argument, must be of format: module:instance")
29
+
30
+ # TODO: find a better, reliable way
31
+ sys.path.append(parsed.directory)
32
+ module = importlib.import_module(split[0])
33
+ app: WampApp = getattr(module, split[1])
34
+ if not isinstance(app, WampApp):
35
+ raise RuntimeError(f"app instance is of unknown type {type(app)}")
36
+
37
+ # uvloop makes things fast.
38
+ uvloop.install()
39
+
40
+ router = Router()
41
+ router.add_realm(parsed.realm)
42
+
43
+ serializer = CBORSerializer()
44
+ client_side_base = ClientSideLocalBaseSession(1, parsed.realm, "local", "local", serializer, router)
45
+ server_side_base = ServerSideLocalBaseSession(1, parsed.realm, "local", "local", serializer)
46
+ server_side_base.set_other(client_side_base)
47
+
48
+ router.attach_client(server_side_base)
49
+
50
+ async def setup():
51
+ session = AsyncSession(client_side_base)
52
+ for procedure, handler in app.procedures.items():
53
+ await session.register(procedure, handler)
54
+ print(f"registered procedure {procedure}")
55
+
56
+ server = Server(router)
57
+ await server.start(parsed.host, parsed.port, start_loop=False)
58
+ print(f"Listening for websocket connections on ws://{parsed.host}:{parsed.port}/ws")
59
+ await asyncio.Event().wait()
60
+
61
+ try:
62
+ asyncio.run(setup())
63
+ except KeyboardInterrupt:
64
+ pass
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
@@ -0,0 +1,51 @@
1
+ import socket
2
+ from typing import Sequence
3
+
4
+ from aiohttp import web
5
+ from wampproto import auth, acceptor
6
+ from websockets import ServerProtocol
7
+ from websockets.sync.server import ServerConnection, Subprotocol
8
+
9
+ from xconn import types, helpers
10
+
11
+
12
+ class WebsocketsAcceptor:
13
+ WSProtocols: Sequence[Subprotocol] = [
14
+ Subprotocol("wamp.2.json"),
15
+ Subprotocol("wamp.2.cbor"),
16
+ Subprotocol("wamp.2.msgpack"),
17
+ ]
18
+
19
+ def __init__(self, authenticator: auth.IServerAuthenticator = None) -> None:
20
+ self.authenticator = authenticator
21
+ self.subprotocols = WebsocketsAcceptor.WSProtocols
22
+
23
+ def accept(self, conn: socket.socket) -> types.BaseSession:
24
+ ws = ServerConnection(conn, ServerProtocol(subprotocols=WebsocketsAcceptor.WSProtocols))
25
+ ws.handshake()
26
+
27
+ serializer = helpers.get_serializer(ws.subprotocol)
28
+ a = acceptor.Acceptor(serializer=serializer, authenticator=self.authenticator)
29
+
30
+ while True:
31
+ data = ws.recv()
32
+ to_send, is_final = a.receive(data)
33
+ ws.send(to_send)
34
+ if is_final:
35
+ return types.BaseSession(ws, a.get_session_details(), serializer)
36
+
37
+
38
+ class AIOHttpAcceptor:
39
+ def __init__(self, authenticator: auth.IServerAuthenticator = None) -> None:
40
+ self.authenticator = authenticator
41
+
42
+ async def accept(self, ws: web.WebSocketResponse) -> types.AIOHttpBaseSession:
43
+ serializer = helpers.get_serializer(ws.ws_protocol)
44
+ a = acceptor.Acceptor(serializer=serializer, authenticator=self.authenticator)
45
+
46
+ while not ws.closed:
47
+ msg = await ws.receive()
48
+ to_send, is_final = a.receive(msg.data)
49
+ await ws.send_bytes(to_send)
50
+ if is_final:
51
+ return types.AIOHttpBaseSession(ws, a.get_session_details(), serializer)
@@ -0,0 +1,29 @@
1
+ from typing import Callable
2
+
3
+
4
+ class IApp:
5
+ @property
6
+ def procedures(self) -> dict[str, Callable]:
7
+ raise NotImplementedError()
8
+
9
+ def register(self, procedure: str):
10
+ raise NotImplementedError()
11
+
12
+
13
+ class WampApp(IApp):
14
+ def __init__(self):
15
+ super().__init__()
16
+ self._procedures = {}
17
+
18
+ @property
19
+ def procedures(self) -> dict[str, Callable]:
20
+ return self._procedures
21
+
22
+ def register(self, procedure: str):
23
+ def _register(func):
24
+ if procedure in self._procedures:
25
+ raise ValueError(f"procedure {procedure} already registered")
26
+
27
+ self._procedures[procedure] = func
28
+
29
+ return _register
@@ -0,0 +1,20 @@
1
+ from wampproto import auth, serializers
2
+
3
+ from xconn.joiner import WebsocketsJoiner
4
+ from xconn.session import Session
5
+
6
+
7
+ class Client:
8
+ def __init__(
9
+ self,
10
+ authenticator: auth.IClientAuthenticator,
11
+ serializer: serializers.Serializer = serializers.JSONSerializer(),
12
+ ):
13
+ self._authenticator = authenticator
14
+ self._serializer = serializer
15
+
16
+ def connect(self, url: str, realm: str) -> Session:
17
+ j = WebsocketsJoiner(self._authenticator, self._serializer)
18
+ details = j.join(url, realm)
19
+
20
+ return Session(details)
@@ -0,0 +1,27 @@
1
+ from wampproto import serializers
2
+
3
+ JSON_SUBPROTOCOL = "wamp.2.json"
4
+ CBOR_SUBPROTOCOL = "wamp.2.cbor"
5
+ MSGPACK_SUBPROTOCOL = "wamp.2.msgpack"
6
+
7
+
8
+ def get_ws_subprotocol(serializer: serializers.Serializer):
9
+ if isinstance(serializer, serializers.JSONSerializer):
10
+ return JSON_SUBPROTOCOL
11
+ elif isinstance(serializer, serializers.CBORSerializer):
12
+ return CBOR_SUBPROTOCOL
13
+ elif isinstance(serializer, serializers.MsgPackSerializer):
14
+ return MSGPACK_SUBPROTOCOL
15
+ else:
16
+ raise ValueError("invalid serializer")
17
+
18
+
19
+ def get_serializer(ws_subprotocol: str) -> serializers.Serializer:
20
+ if ws_subprotocol == JSON_SUBPROTOCOL:
21
+ return serializers.JSONSerializer()
22
+ elif ws_subprotocol == CBOR_SUBPROTOCOL:
23
+ return serializers.CBORSerializer()
24
+ elif ws_subprotocol == MSGPACK_SUBPROTOCOL:
25
+ return serializers.MsgPackSerializer()
26
+ else:
27
+ raise ValueError(f"invalid websocket subprotocol {ws_subprotocol}")
@@ -0,0 +1,29 @@
1
+ from websockets.sync.client import connect
2
+ from wampproto import joiner, serializers, auth
3
+ from wampproto.joiner import Joiner
4
+
5
+ from xconn import types, helpers
6
+
7
+
8
+ class WebsocketsJoiner:
9
+ def __init__(
10
+ self,
11
+ authenticator: auth.IClientAuthenticator = None,
12
+ serializer: serializers.Serializer = serializers.JSONSerializer(),
13
+ ):
14
+ self._authenticator = authenticator
15
+ self._serializer = serializer
16
+
17
+ def join(self, uri: str, realm: str) -> types.BaseSession:
18
+ ws = connect(uri, subprotocols=[helpers.get_ws_subprotocol(serializer=self._serializer)])
19
+
20
+ j: Joiner = joiner.Joiner(realm, serializer=self._serializer, authenticator=self._authenticator)
21
+ ws.send(j.send_hello())
22
+
23
+ while True:
24
+ data = ws.recv()
25
+ to_send = j.receive(data)
26
+ if to_send is None:
27
+ return types.BaseSession(ws, j.get_session_details(), self._serializer)
28
+
29
+ ws.send(to_send)
@@ -0,0 +1,75 @@
1
+ from asyncio import gather
2
+
3
+ from wampproto.types import MessageWithRecipient
4
+ from wampproto import dealer, broker, messages
5
+
6
+ from xconn import types
7
+
8
+
9
+ class Realm:
10
+ def __init__(self):
11
+ super().__init__()
12
+ self.dealer = dealer.Dealer()
13
+ self.broker = broker.Broker()
14
+
15
+ self.clients: dict[int, types.IAsyncBaseSession] = {}
16
+
17
+ def attach_client(self, base: types.IAsyncBaseSession):
18
+ self.clients[base.id] = base
19
+ self.dealer.add_session(base.id)
20
+ self.broker.add_session(base.id)
21
+
22
+ def detach_client(self, base: types.IAsyncBaseSession):
23
+ del self.clients[base.id]
24
+ self.broker.remove_session(base.id)
25
+ self.dealer.remove_session(base.id)
26
+
27
+ def stop(self):
28
+ """stop will disconnect all clients."""
29
+ pass
30
+
31
+ async def receive_message(self, session_id: int, msg: messages.Message):
32
+ match msg.TYPE:
33
+ case messages.Call.TYPE | messages.Yield.TYPE | messages.Register.TYPE | messages.UnRegister.TYPE:
34
+ recipient = self.dealer.receive_message(session_id, msg)
35
+ client = self.clients[recipient.recipient]
36
+ await client.send_message(recipient.message)
37
+
38
+ case messages.Publish.TYPE:
39
+ recipients = self.broker.receive_message(session_id, msg)
40
+ if recipients is None:
41
+ # no subscribers AND "acknowledge=False"
42
+ return
43
+
44
+ # if the publish options had "acknowledge=True" the last one
45
+ # should be PUBLISHED.
46
+ published: MessageWithRecipient | None = None
47
+ if isinstance(recipients[-1].message, messages.Published):
48
+ published = recipients.pop(-1)
49
+
50
+ tasks = []
51
+ for recipient in recipients:
52
+ client = self.clients[recipient.recipient]
53
+ tasks.append(client.send_message(recipient.message))
54
+
55
+ await gather(*tasks)
56
+
57
+ if published is not None:
58
+ client = self.clients[published.recipient]
59
+ await client.send_message(published.message)
60
+
61
+ case messages.Subscribe.TYPE | messages.UnSubscribe.TYPE:
62
+ recipients = self.broker.receive_message(session_id, msg)
63
+ recipient = recipients[0]
64
+
65
+ client = self.clients[recipient.recipient]
66
+ await client.send_message(recipient.message)
67
+ case messages.Goodbye.TYPE:
68
+ self.dealer.remove_session(session_id)
69
+ self.broker.remove_session(session_id)
70
+ try:
71
+ client = self.clients.pop(session_id)
72
+ except KeyError:
73
+ return
74
+
75
+ await client.close()
@@ -0,0 +1,36 @@
1
+ from wampproto import messages
2
+
3
+ from xconn import realm, types
4
+
5
+
6
+ class Router:
7
+ def __init__(self):
8
+ super().__init__()
9
+ self.realms: dict[str, realm.Realm] = {}
10
+
11
+ def add_realm(self, name: str):
12
+ self.realms[name] = realm.Realm()
13
+
14
+ def remove_realm(self, name: str):
15
+ del self.realms[name]
16
+
17
+ def has_realm(self, name: str):
18
+ return name in self.realms
19
+
20
+ def attach_client(self, base_session: types.IAsyncBaseSession):
21
+ if base_session.realm not in self.realms:
22
+ raise ValueError(f"cannot attach client to non-existent realm {base_session.realm}")
23
+
24
+ self.realms[base_session.realm].attach_client(base_session)
25
+
26
+ def detach_client(self, base_session: types.IAsyncBaseSession):
27
+ if base_session.realm not in self.realms:
28
+ raise ValueError(f"cannot detach client from non-existent realm {base_session.realm}")
29
+
30
+ self.realms[base_session.realm].detach_client(base_session)
31
+
32
+ async def receive_message(self, base_session: types.IAsyncBaseSession, msg: messages.Message):
33
+ if base_session.realm not in self.realms:
34
+ raise ValueError(f"cannot process message for non-existent realm {base_session.realm}")
35
+
36
+ await self.realms[base_session.realm].receive_message(base_session.id, msg)
@@ -0,0 +1,46 @@
1
+ import aiohttp
2
+ from aiohttp import web
3
+
4
+ from xconn.router import Router
5
+ from xconn.acceptor import AIOHttpAcceptor
6
+
7
+
8
+ class Server:
9
+ def __init__(self, router: Router):
10
+ self.router = router
11
+
12
+ async def _websocket_handler(self, request):
13
+ ws = web.WebSocketResponse(protocols=["wamp.2.json", "wamp.2.cbor", "wamp.2.msgpack"])
14
+ # upgrade this connection to websocket.
15
+ await ws.prepare(request)
16
+
17
+ acceptor = AIOHttpAcceptor()
18
+ base_session = await acceptor.accept(ws)
19
+ self.router.attach_client(base_session)
20
+
21
+ while not ws.closed:
22
+ msg = await ws.receive()
23
+
24
+ if msg.type == aiohttp.WSMsgType.TEXT or msg.type == aiohttp.WSMsgType.BINARY:
25
+ msg = base_session.serializer.deserialize(msg.data)
26
+ await self.router.receive_message(base_session, msg)
27
+ elif msg.type == aiohttp.WSMsgType.ERROR:
28
+ print(f"Error: {msg.exception()}")
29
+ elif msg.type == aiohttp.WSMsgType.CLOSE:
30
+ print("Client disconnected")
31
+ break
32
+
33
+ return ws
34
+
35
+ async def start(self, host: str, port: int, start_loop: bool = False):
36
+ app = web.Application()
37
+ app.router.add_get("/ws", self._websocket_handler)
38
+
39
+ if start_loop:
40
+ web.run_app(app, host=host, port=port)
41
+ else:
42
+ runner = web.AppRunner(app)
43
+ await runner.setup()
44
+
45
+ site = aiohttp.web.TCPSite(runner, host=host, port=port)
46
+ await site.start()
@@ -0,0 +1,225 @@
1
+ import asyncio
2
+ from concurrent.futures import Future
3
+ from threading import Thread
4
+ from typing import Callable, Any
5
+
6
+ from wampproto import messages, idgen, session
7
+
8
+ from xconn import types
9
+
10
+
11
+ class Session:
12
+ def __init__(self, base_session: types.BaseSession):
13
+ # RPC data structures
14
+ self.call_requests: dict[int, Future[types.Result]] = {}
15
+ self.register_requests: dict[int, types.RegisterRequest] = {}
16
+ self.registrations: dict[int, Callable[[types.Invocation], types.Result]] = {}
17
+ self.unregister_requests: dict[int, types.UnregisterRequest] = {}
18
+
19
+ # PubSub data structures
20
+ self.publish_requests: dict[int, Future[None]] = {}
21
+ self.subscribe_requests: dict[int, types.SubscribeRequest] = {}
22
+ self.subscriptions: dict[int, Callable[[types.Event], None]] = {}
23
+ self.unsubscribe_requests: dict[int, types.UnsubscribeRequest] = {}
24
+
25
+ # ID generator
26
+ self.idgen = idgen.SessionScopeIDGenerator()
27
+
28
+ self.base_session = base_session
29
+
30
+ # initialize the sans-io wamp session
31
+ self.session = session.WAMPSession(base_session.serializer)
32
+
33
+ thread = Thread(target=self.wait)
34
+ thread.start()
35
+
36
+ def wait(self):
37
+ while True:
38
+ try:
39
+ data = self.base_session.receive()
40
+ except Exception:
41
+ break
42
+
43
+ self.process_incoming_message(self.session.receive(data))
44
+
45
+ def process_incoming_message(self, msg: messages.Message):
46
+ if isinstance(msg, messages.Registered):
47
+ request = self.register_requests.pop(msg.request_id)
48
+ self.registrations[msg.registration_id] = request.endpoint
49
+ request.future.set_result(types.Registration(msg.registration_id))
50
+ elif isinstance(msg, messages.UnRegistered):
51
+ request = self.unregister_requests.pop(msg.request_id)
52
+ del self.registrations[request.registration_id]
53
+ request.future.set_result(None)
54
+ elif isinstance(msg, messages.Result):
55
+ request = self.call_requests.pop(msg.request_id)
56
+ request.set_result(types.Result(msg.args, msg.kwargs, msg.options))
57
+ elif isinstance(msg, messages.Invocation):
58
+ endpoint = self.registrations[msg.registration_id]
59
+ result = endpoint(msg)
60
+ data = self.session.send_message(messages.Yield(msg.request_id, result.args, result.kwargs, result.details))
61
+ self.base_session.send(data)
62
+ elif isinstance(msg, messages.Subscribed):
63
+ request = self.subscribe_requests.pop(msg.request_id)
64
+ self.subscriptions[msg.subscription_id] = request.endpoint
65
+ request.future.set_result(types.Subscription(msg.subscription_id))
66
+ elif isinstance(msg, messages.UnSubscribed):
67
+ request = self.unsubscribe_requests.pop(msg.request_id)
68
+ del self.subscriptions[request.subscription_id]
69
+ request.future.set_result(None)
70
+ elif isinstance(msg, messages.Published):
71
+ request = self.publish_requests.pop(msg.request_id)
72
+ request.set_result(None)
73
+ elif isinstance(msg, messages.Event):
74
+ endpoint = self.subscriptions[msg.subscription_id]
75
+ endpoint(types.Event(msg.args, msg.kwargs, msg.details))
76
+ elif isinstance(msg, messages.Error):
77
+ pass
78
+ else:
79
+ raise ValueError("received unknown message")
80
+
81
+ def call(self, procedure: str, args: list[Any] = None, kwargs: dict = None, options: dict = None) -> types.Result:
82
+ call = messages.Call(self.idgen.next(), procedure, args, kwargs, options)
83
+ data = self.session.send_message(call)
84
+
85
+ f = Future()
86
+ self.call_requests[call.request_id] = f
87
+ self.base_session.send(data)
88
+
89
+ return f.result()
90
+
91
+ def register(self, procedure: str, endpoint: Callable[[types.Invocation], types.Result]) -> types.Registration:
92
+ register = messages.Register(self.idgen.next(), procedure)
93
+ data = self.session.send_message(register)
94
+
95
+ f: Future[types.Registration] = Future()
96
+ self.register_requests[register.request_id] = types.RegisterRequest(f, endpoint)
97
+ self.base_session.send(data)
98
+
99
+ return f.result()
100
+
101
+ def unregister(self, reg: types.Registration):
102
+ unregister = messages.UnRegister(self.idgen.next(), reg.registration_id)
103
+ data = self.session.send_message(unregister)
104
+
105
+ f: Future = Future()
106
+ self.unregister_requests[unregister.request_id] = types.UnregisterRequest(f, reg.registration_id)
107
+ self.base_session.send(data)
108
+
109
+ f.result()
110
+
111
+ def subscribe(self, topic: str, endpoint: Callable[[types.Event], None]) -> types.Subscription:
112
+ subscribe = messages.Subscribe(self.idgen.next(), topic)
113
+ data = self.session.send_message(subscribe)
114
+
115
+ f: Future[types.Subscription] = Future()
116
+ self.subscribe_requests[subscribe.request_id] = types.SubscribeRequest(f, endpoint)
117
+ self.base_session.send(data)
118
+
119
+ return f.result()
120
+
121
+ def unsubscribe(self, sub: types.Subscription):
122
+ unsubscribe = messages.UnSubscribe(self.idgen.next(), sub.subscription_id)
123
+ data = self.session.send_message(unsubscribe)
124
+
125
+ f: Future = Future()
126
+ self.unsubscribe_requests[unsubscribe.request_id] = types.UnsubscribeRequest(f, sub.subscription_id)
127
+ self.base_session.send(data)
128
+
129
+ f.result()
130
+
131
+ def publish(self, topic: str, args: list[Any] = None, kwargs: dict = None, options: dict = None):
132
+ publish = messages.Publish(self.idgen.next(), topic, args, kwargs, options)
133
+ data = self.session.send_message(publish)
134
+
135
+ if options is not None and options.get("acknowledge", True):
136
+ f: Future = Future()
137
+ self.publish_requests[publish.request_id] = f
138
+ self.base_session.send(data)
139
+ return f.result()
140
+
141
+ self.base_session.send(data)
142
+
143
+
144
+ class AsyncSession:
145
+ def __init__(self, base_session: types.IAsyncBaseSession):
146
+ # RPC data structures
147
+ self.call_requests: dict[int, Future[types.Result]] = {}
148
+ self.register_requests: dict[int, types.RegisterRequest] = {}
149
+ self.registrations: dict[int, Callable[[types.Invocation], types.Result]] = {}
150
+ self.unregister_requests: dict[int, types.UnregisterRequest] = {}
151
+
152
+ # PubSub data structures
153
+ self.publish_requests: dict[int, Future[None]] = {}
154
+ self.subscribe_requests: dict[int, types.SubscribeRequest] = {}
155
+ self.subscriptions: dict[int, Callable[[types.Event], None]] = {}
156
+ self.unsubscribe_requests: dict[int, types.UnsubscribeRequest] = {}
157
+
158
+ # ID generator
159
+ self.idgen = idgen.SessionScopeIDGenerator()
160
+
161
+ self.base_session = base_session
162
+
163
+ # initialize the sans-io wamp session
164
+ self.session = session.WAMPSession(base_session.serializer)
165
+
166
+ loop = asyncio.get_event_loop()
167
+ loop.create_task(self.wait())
168
+
169
+ async def wait(self):
170
+ while True:
171
+ try:
172
+ data = await self.base_session.receive()
173
+ except Exception as e:
174
+ print(e)
175
+ break
176
+
177
+ await self.process_incoming_message(self.session.receive(data))
178
+
179
+ async def process_incoming_message(self, msg: messages.Message):
180
+ if isinstance(msg, messages.Registered):
181
+ request = self.register_requests.pop(msg.request_id)
182
+ self.registrations[msg.registration_id] = request.endpoint
183
+ request.future.set_result(types.Registration(msg.registration_id))
184
+ elif isinstance(msg, messages.UnRegistered):
185
+ request = self.unregister_requests.pop(msg.request_id)
186
+ del self.registrations[request.registration_id]
187
+ request.future.set_result(None)
188
+ elif isinstance(msg, messages.Result):
189
+ request = self.call_requests.pop(msg.request_id)
190
+ request.set_result(types.Result(msg.args, msg.kwargs, msg.options))
191
+ elif isinstance(msg, messages.Invocation):
192
+ endpoint = self.registrations[msg.registration_id]
193
+ result = endpoint(msg)
194
+ data = self.session.send_message(messages.Yield(msg.request_id, result.args, result.kwargs, result.details))
195
+ await self.base_session.send(data)
196
+ elif isinstance(msg, messages.Subscribed):
197
+ request = self.subscribe_requests.pop(msg.request_id)
198
+ self.subscriptions[msg.subscription_id] = request.endpoint
199
+ request.future.set_result(types.Subscription(msg.subscription_id))
200
+ elif isinstance(msg, messages.UnSubscribed):
201
+ request = self.unsubscribe_requests.pop(msg.request_id)
202
+ del self.subscriptions[request.subscription_id]
203
+ request.future.set_result(None)
204
+ elif isinstance(msg, messages.Published):
205
+ request = self.publish_requests.pop(msg.request_id)
206
+ request.set_result(None)
207
+ elif isinstance(msg, messages.Event):
208
+ endpoint = self.subscriptions[msg.subscription_id]
209
+ endpoint(types.Event(msg.args, msg.kwargs, msg.details))
210
+ elif isinstance(msg, messages.Error):
211
+ pass
212
+ else:
213
+ raise ValueError("received unknown message")
214
+
215
+ async def register(
216
+ self, procedure: str, endpoint: Callable[[types.Invocation], types.Result]
217
+ ) -> Future[types.Registration]:
218
+ register = messages.Register(self.idgen.next(), procedure, options={})
219
+ data = self.session.send_message(register)
220
+
221
+ f: Future[types.Registration] = Future()
222
+ self.register_requests[register.request_id] = types.RegisterRequest(f, endpoint)
223
+ await self.base_session.send(data)
224
+
225
+ return f
@@ -0,0 +1,357 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from asyncio import Future
5
+ from collections import deque
6
+ from dataclasses import dataclass
7
+ from typing import Callable
8
+
9
+ from aiohttp import web
10
+ from websockets.sync.connection import Connection
11
+ from wampproto import messages, joiner, serializers
12
+
13
+
14
+ @dataclass
15
+ class Registration:
16
+ registration_id: int
17
+
18
+
19
+ @dataclass
20
+ class RegisterRequest:
21
+ future: Future[Registration]
22
+ endpoint: Callable[[Invocation], Result]
23
+
24
+
25
+ @dataclass
26
+ class UnregisterRequest:
27
+ future: Future
28
+ registration_id: int
29
+
30
+
31
+ @dataclass
32
+ class Subscription:
33
+ subscription_id: int
34
+
35
+
36
+ @dataclass
37
+ class SubscribeRequest:
38
+ future: Future[Subscription]
39
+ endpoint: Callable[[Event], None]
40
+
41
+
42
+ @dataclass
43
+ class UnsubscribeRequest:
44
+ future: Future
45
+ subscription_id: int
46
+
47
+
48
+ @dataclass
49
+ class Result:
50
+ args: list | None = None
51
+ kwargs: dict | None = None
52
+ details: dict | None = None
53
+
54
+
55
+ @dataclass
56
+ class Invocation:
57
+ args: list | None
58
+ kwargs: dict | None
59
+ details: dict | None
60
+
61
+
62
+ @dataclass
63
+ class Event:
64
+ args: list | None
65
+ kwargs: dict | None
66
+ details: dict | None
67
+
68
+
69
+ class IBasePeer:
70
+ def read(self) -> str | bytes:
71
+ raise NotImplementedError()
72
+
73
+ def write(self, data: str | bytes):
74
+ raise NotImplementedError()
75
+
76
+
77
+ class BasePeer(IBasePeer):
78
+ def read(self) -> str | bytes:
79
+ return super().read()
80
+
81
+ def write(self, data: str | bytes):
82
+ super().write(data)
83
+
84
+
85
+ class IPeer:
86
+ def read(self) -> str | bytes:
87
+ raise NotImplementedError()
88
+
89
+ def write(self, data: str | bytes):
90
+ raise NotImplementedError()
91
+
92
+ def read_message(self) -> messages.Message:
93
+ raise NotImplementedError()
94
+
95
+ def write_message(self, msg: messages.Message):
96
+ raise NotImplementedError()
97
+
98
+
99
+ class Peer(IPeer):
100
+ def __init__(self, base_peer: BasePeer):
101
+ super().__init__()
102
+ self._base_peer = base_peer
103
+
104
+
105
+ class IBaseSession:
106
+ @property
107
+ def id(self) -> int:
108
+ raise NotImplementedError()
109
+
110
+ @property
111
+ def realm(self) -> str:
112
+ raise NotImplementedError()
113
+
114
+ @property
115
+ def authid(self) -> str:
116
+ raise NotImplementedError()
117
+
118
+ @property
119
+ def authrole(self) -> str:
120
+ raise NotImplementedError()
121
+
122
+ def send(self, data: bytes):
123
+ raise NotImplementedError()
124
+
125
+ def receive(self) -> bytes:
126
+ raise NotImplementedError()
127
+
128
+ def send_message(self, msg: messages.Message):
129
+ raise NotImplementedError()
130
+
131
+ def receive_message(self) -> messages.Message:
132
+ raise NotImplementedError()
133
+
134
+ def close(self):
135
+ raise NotImplementedError()
136
+
137
+
138
+ class BaseSession(IBaseSession):
139
+ def __init__(self, ws: Connection, session_details: joiner.SessionDetails, serializer: serializers.Serializer):
140
+ super().__init__()
141
+ self.ws = ws
142
+ self.session_details = session_details
143
+ self.serializer = serializer
144
+
145
+ @property
146
+ def id(self) -> int:
147
+ return self.session_details.session_id
148
+
149
+ @property
150
+ def realm(self) -> str:
151
+ return self.session_details.realm
152
+
153
+ @property
154
+ def authid(self) -> str:
155
+ return self.session_details.authid
156
+
157
+ @property
158
+ def authrole(self) -> str:
159
+ return self.session_details.authrole
160
+
161
+ def send(self, data: bytes):
162
+ self.ws.send(data)
163
+
164
+ def receive(self) -> bytes:
165
+ return self.ws.recv()
166
+
167
+ def send_message(self, msg: messages.Message):
168
+ self.ws.send(self.serializer.serialize(msg))
169
+
170
+ def receive_message(self) -> messages.Message:
171
+ return self.serializer.deserialize(self.receive())
172
+
173
+ def close(self):
174
+ self.ws.close()
175
+
176
+
177
+ class IAsyncBaseSession:
178
+ @property
179
+ def id(self) -> int:
180
+ raise NotImplementedError()
181
+
182
+ @property
183
+ def realm(self) -> str:
184
+ raise NotImplementedError()
185
+
186
+ @property
187
+ def authid(self) -> str:
188
+ raise NotImplementedError()
189
+
190
+ @property
191
+ def authrole(self) -> str:
192
+ raise NotImplementedError()
193
+
194
+ @property
195
+ def serializer(self) -> serializers.Serializer:
196
+ raise NotImplementedError()
197
+
198
+ async def send(self, data: bytes | str):
199
+ raise NotImplementedError()
200
+
201
+ async def receive(self) -> bytes | str:
202
+ raise NotImplementedError()
203
+
204
+ async def send_message(self, msg: messages.Message):
205
+ raise NotImplementedError()
206
+
207
+ async def receive_message(self) -> messages.Message:
208
+ raise NotImplementedError()
209
+
210
+ async def close(self):
211
+ raise NotImplementedError()
212
+
213
+
214
+ class AIOHttpBaseSession(IAsyncBaseSession):
215
+ def __init__(
216
+ self, ws: web.WebSocketResponse, session_details: joiner.SessionDetails, serializer: serializers.Serializer
217
+ ):
218
+ super().__init__()
219
+ self.ws = ws
220
+ self.session_details = session_details
221
+ self._serializer = serializer
222
+
223
+ @property
224
+ def id(self) -> int:
225
+ return self.session_details.session_id
226
+
227
+ @property
228
+ def realm(self) -> str:
229
+ return self.session_details.realm
230
+
231
+ @property
232
+ def authid(self) -> str:
233
+ return self.session_details.authid
234
+
235
+ @property
236
+ def authrole(self) -> str:
237
+ return self.session_details.authrole
238
+
239
+ @property
240
+ def serializer(self) -> serializers.Serializer:
241
+ return self._serializer
242
+
243
+ async def send(self, data: bytes):
244
+ await self.ws.send_bytes(data)
245
+
246
+ async def receive(self) -> bytes:
247
+ return await self.ws.receive_bytes()
248
+
249
+ async def send_message(self, msg: messages.Message):
250
+ await self.send(self.serializer.serialize(msg))
251
+
252
+ async def receive_message(self) -> messages.Message:
253
+ return self.serializer.deserialize(await self.receive())
254
+
255
+ async def close(self):
256
+ await self.ws.close()
257
+
258
+
259
+ class ClientSideLocalBaseSession(IAsyncBaseSession):
260
+ def __init__(self, sid: int, realm: str, authid: str, authrole: str, serializer: serializers.Serializer, router):
261
+ super().__init__()
262
+ self._sid: int = sid
263
+ self._realm: str = realm
264
+ self._authid: str = authid
265
+ self._authrole: str = authrole
266
+ self._serializer: serializers.Serializer = serializer
267
+ self._router = router
268
+
269
+ self._incoming_messages = deque()
270
+ self._cond = asyncio.Condition()
271
+
272
+ @property
273
+ def id(self) -> int:
274
+ return self._sid
275
+
276
+ @property
277
+ def realm(self) -> str:
278
+ return self._realm
279
+
280
+ @property
281
+ def authid(self) -> str:
282
+ return self._authid
283
+
284
+ @property
285
+ def authrole(self) -> str:
286
+ return self._authrole
287
+
288
+ @property
289
+ def serializer(self) -> serializers.Serializer:
290
+ return self._serializer
291
+
292
+ async def send(self, data: bytes | str):
293
+ await self.send_message(self.serializer.deserialize(data))
294
+
295
+ async def receive(self) -> bytes | str:
296
+ async with self._cond:
297
+ while not self._incoming_messages:
298
+ await self._cond.wait()
299
+
300
+ return self._incoming_messages.popleft()
301
+
302
+ async def send_message(self, msg: messages.Message):
303
+ await self._router.receive_message(self, msg)
304
+
305
+ async def receive_message(self) -> messages.Message:
306
+ return self.serializer.deserialize(await self.receive())
307
+
308
+ async def close(self):
309
+ pass
310
+
311
+ async def feed(self, data: bytes | str):
312
+ async with self._cond:
313
+ self._incoming_messages.append(data)
314
+ self._cond.notify()
315
+
316
+
317
+ class ServerSideLocalBaseSession(IAsyncBaseSession):
318
+ def __init__(self, sid: int, realm: str, authid: str, authrole: str, serializer: serializers.Serializer):
319
+ super().__init__()
320
+ self._sid: int = sid
321
+ self._realm: str = realm
322
+ self._authid: str = authid
323
+ self._authrole: str = authrole
324
+ self._serializer: serializers.Serializer = serializer
325
+ self._other: ClientSideLocalBaseSession = None
326
+
327
+ def set_other(self, other: ClientSideLocalBaseSession):
328
+ self._other = other
329
+
330
+ @property
331
+ def id(self) -> int:
332
+ return self._sid
333
+
334
+ @property
335
+ def realm(self) -> str:
336
+ return self._realm
337
+
338
+ @property
339
+ def authid(self) -> str:
340
+ return self._authid
341
+
342
+ @property
343
+ def authrole(self) -> str:
344
+ return self._authrole
345
+
346
+ @property
347
+ def serializer(self) -> serializers.Serializer:
348
+ return self._serializer
349
+
350
+ async def send(self, data: bytes | str):
351
+ await self._other.feed(data)
352
+
353
+ async def send_message(self, msg: messages.Message):
354
+ await self.send(self.serializer.serialize(msg))
355
+
356
+ async def close(self):
357
+ pass
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.1
2
+ Name: xconn
3
+ Version: 0.1.0
4
+ Summary: https://github.com/xconnio/xconn-python
5
+ Author-email: Omer Akram <omer@thing.com>, Mahad Munir <mahad@xconn.io>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 XConnIO
9
+ Copyright (c) 2024 Simple Things Inc.
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Project-URL: Homepage, https://github.com/xconnio/xconn-python
30
+ Project-URL: Bug Tracker, https://github.com/xconnio/xconn-python/issues
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Programming Language :: Python :: 3.10
34
+ Classifier: Programming Language :: Python :: 3.11
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Requires-Python: >=3.10
37
+ Description-Content-Type: text/markdown
38
+ License-File: LICENSE
39
+ Requires-Dist: websockets
40
+ Requires-Dist: wampproto
41
+ Requires-Dist: aiohttp
42
+ Requires-Dist: uvloop
43
+ Provides-Extra: test
44
+ Requires-Dist: coverage; extra == "test"
45
+ Requires-Dist: pytest; extra == "test"
46
+ Requires-Dist: pytest-asyncio; extra == "test"
47
+ Requires-Dist: ruff; extra == "test"
48
+
49
+ # XConn Python
@@ -0,0 +1,21 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ xconn/__init__.py
5
+ xconn/__main__.py
6
+ xconn/acceptor.py
7
+ xconn/app.py
8
+ xconn/client.py
9
+ xconn/helpers.py
10
+ xconn/joiner.py
11
+ xconn/realm.py
12
+ xconn/router.py
13
+ xconn/server.py
14
+ xconn/session.py
15
+ xconn/types.py
16
+ xconn.egg-info/PKG-INFO
17
+ xconn.egg-info/SOURCES.txt
18
+ xconn.egg-info/dependency_links.txt
19
+ xconn.egg-info/entry_points.txt
20
+ xconn.egg-info/requires.txt
21
+ xconn.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ xconn = xconn.__main__:main
@@ -0,0 +1,10 @@
1
+ websockets
2
+ wampproto
3
+ aiohttp
4
+ uvloop
5
+
6
+ [test]
7
+ coverage
8
+ pytest
9
+ pytest-asyncio
10
+ ruff
@@ -0,0 +1 @@
1
+ xconn