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 +22 -0
- xconn-0.1.0/PKG-INFO +49 -0
- xconn-0.1.0/README.md +1 -0
- xconn-0.1.0/pyproject.toml +43 -0
- xconn-0.1.0/setup.cfg +4 -0
- xconn-0.1.0/xconn/__init__.py +0 -0
- xconn-0.1.0/xconn/__main__.py +68 -0
- xconn-0.1.0/xconn/acceptor.py +51 -0
- xconn-0.1.0/xconn/app.py +29 -0
- xconn-0.1.0/xconn/client.py +20 -0
- xconn-0.1.0/xconn/helpers.py +27 -0
- xconn-0.1.0/xconn/joiner.py +29 -0
- xconn-0.1.0/xconn/realm.py +75 -0
- xconn-0.1.0/xconn/router.py +36 -0
- xconn-0.1.0/xconn/server.py +46 -0
- xconn-0.1.0/xconn/session.py +225 -0
- xconn-0.1.0/xconn/types.py +357 -0
- xconn-0.1.0/xconn.egg-info/PKG-INFO +49 -0
- xconn-0.1.0/xconn.egg-info/SOURCES.txt +21 -0
- xconn-0.1.0/xconn.egg-info/dependency_links.txt +1 -0
- xconn-0.1.0/xconn.egg-info/entry_points.txt +2 -0
- xconn-0.1.0/xconn.egg-info/requires.txt +10 -0
- xconn-0.1.0/xconn.egg-info/top_level.txt +1 -0
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
|
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)
|
xconn-0.1.0/xconn/app.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xconn
|