xconn 0.3.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.
- xconn/__init__.py +40 -0
- xconn/__main__.py +20 -0
- xconn/_client/__init__.py +0 -0
- xconn/_client/async_.py +183 -0
- xconn/_client/cli.py +176 -0
- xconn/_client/helpers.py +564 -0
- xconn/_client/sync.py +189 -0
- xconn/_client/types.py +34 -0
- xconn/acceptor.py +66 -0
- xconn/app.py +172 -0
- xconn/async_session.py +280 -0
- xconn/client.py +98 -0
- xconn/exception.py +24 -0
- xconn/helpers.py +52 -0
- xconn/joiner.py +112 -0
- xconn/realm.py +75 -0
- xconn/router.py +36 -0
- xconn/server.py +86 -0
- xconn/session.py +253 -0
- xconn/transports.py +333 -0
- xconn/types.py +699 -0
- xconn/uris.py +5 -0
- xconn/utils.py +21 -0
- xconn-0.3.0.dist-info/METADATA +75 -0
- xconn-0.3.0.dist-info/RECORD +29 -0
- xconn-0.3.0.dist-info/WHEEL +5 -0
- xconn-0.3.0.dist-info/entry_points.txt +2 -0
- xconn-0.3.0.dist-info/licenses/LICENSE +22 -0
- xconn-0.3.0.dist-info/top_level.txt +1 -0
xconn/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from xconn.app import App, Component, register, subscribe
|
|
2
|
+
from xconn.client import Client, AsyncClient
|
|
3
|
+
from xconn.router import Router
|
|
4
|
+
from xconn.server import Server
|
|
5
|
+
from xconn.utils import run
|
|
6
|
+
|
|
7
|
+
from wampproto.auth import (
|
|
8
|
+
AnonymousAuthenticator,
|
|
9
|
+
TicketAuthenticator,
|
|
10
|
+
WAMPCRAAuthenticator,
|
|
11
|
+
CryptoSignAuthenticator,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from wampproto.serializers import (
|
|
15
|
+
JSONSerializer,
|
|
16
|
+
MsgPackSerializer,
|
|
17
|
+
CBORSerializer,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"App",
|
|
22
|
+
"Component",
|
|
23
|
+
"register",
|
|
24
|
+
"subscribe",
|
|
25
|
+
"Client",
|
|
26
|
+
"AsyncClient",
|
|
27
|
+
"Router",
|
|
28
|
+
"Server",
|
|
29
|
+
# export authenticators
|
|
30
|
+
"AnonymousAuthenticator",
|
|
31
|
+
"TicketAuthenticator",
|
|
32
|
+
"WAMPCRAAuthenticator",
|
|
33
|
+
"CryptoSignAuthenticator",
|
|
34
|
+
# export serializers
|
|
35
|
+
"JSONSerializer",
|
|
36
|
+
"MsgPackSerializer",
|
|
37
|
+
"CBORSerializer",
|
|
38
|
+
# runner for async client
|
|
39
|
+
"run",
|
|
40
|
+
]
|
xconn/__main__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from xconn._client.cli import add_client_subparser
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(description="XConn CLI")
|
|
8
|
+
subparsers = parser.add_subparsers(dest="component")
|
|
9
|
+
|
|
10
|
+
# Add subcommands from other modules
|
|
11
|
+
add_client_subparser(subparsers)
|
|
12
|
+
|
|
13
|
+
args = parser.parse_args()
|
|
14
|
+
|
|
15
|
+
if hasattr(args, "func"):
|
|
16
|
+
args.func(args)
|
|
17
|
+
elif hasattr(args, "print_help"):
|
|
18
|
+
args.print_help()
|
|
19
|
+
else:
|
|
20
|
+
parser.print_help()
|
|
File without changes
|
xconn/_client/async_.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import random
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import AsyncGenerator
|
|
6
|
+
|
|
7
|
+
from xconn import App
|
|
8
|
+
from xconn._client.helpers import (
|
|
9
|
+
_validate_procedure_function,
|
|
10
|
+
_validate_topic_function,
|
|
11
|
+
_handle_result,
|
|
12
|
+
_sanitize_incoming_data,
|
|
13
|
+
collect_docs,
|
|
14
|
+
select_authenticator,
|
|
15
|
+
start_server_async,
|
|
16
|
+
handle_model_validation,
|
|
17
|
+
ensure_caller_allowed,
|
|
18
|
+
INITIAL_WAIT,
|
|
19
|
+
MAX_WAIT,
|
|
20
|
+
ProcedureMetadata,
|
|
21
|
+
assemble_call_details,
|
|
22
|
+
assemble_event_details,
|
|
23
|
+
validate_invocation_parameters,
|
|
24
|
+
validate_event_parameters,
|
|
25
|
+
)
|
|
26
|
+
from xconn._client.types import ClientConfig
|
|
27
|
+
from xconn.client import AsyncClient
|
|
28
|
+
from xconn.async_session import AsyncSession
|
|
29
|
+
from xconn.types import Event, Invocation, Result, RegisterOptions, InvokeOptions
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def _setup(app: App, session: AsyncSession):
|
|
33
|
+
app.set_session(session)
|
|
34
|
+
|
|
35
|
+
for uri, func in app.procedures.items():
|
|
36
|
+
await register_async(session, uri, func)
|
|
37
|
+
|
|
38
|
+
for uri, func in app.topics.items():
|
|
39
|
+
await subscribe_async(session, uri, func)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def connect_async(app: App, config: ClientConfig, start_router: bool = False):
|
|
43
|
+
if start_router:
|
|
44
|
+
await start_server_async(config)
|
|
45
|
+
|
|
46
|
+
auth = select_authenticator(config)
|
|
47
|
+
client = AsyncClient(authenticator=auth, ws_config=config.websocket_config)
|
|
48
|
+
|
|
49
|
+
async def wait_and_connect(previous_wait: float = INITIAL_WAIT):
|
|
50
|
+
next_wait = min(random.uniform(INITIAL_WAIT, previous_wait * 3), MAX_WAIT)
|
|
51
|
+
print(f"reconnecting in {next_wait:.1f} seconds...")
|
|
52
|
+
await asyncio.sleep(next_wait)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
new_session = await client.connect(config.url, config.realm, on_connect, on_disconnect)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(e)
|
|
58
|
+
|
|
59
|
+
await wait_and_connect(next_wait)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
await _setup(app, new_session)
|
|
63
|
+
|
|
64
|
+
async def on_connect():
|
|
65
|
+
print("connected", config.realm)
|
|
66
|
+
|
|
67
|
+
async def on_disconnect():
|
|
68
|
+
print("disconnected", config.realm)
|
|
69
|
+
await wait_and_connect()
|
|
70
|
+
|
|
71
|
+
session = await client.connect(config.url, config.realm, on_connect, on_disconnect)
|
|
72
|
+
await _setup(app, session)
|
|
73
|
+
|
|
74
|
+
if app.schema_procedure is not None and app.schema_procedure != "":
|
|
75
|
+
docs = []
|
|
76
|
+
|
|
77
|
+
for uri, func in app.procedures.items():
|
|
78
|
+
docs.append(collect_docs(uri, func, "procedure"))
|
|
79
|
+
|
|
80
|
+
for uri, func in app.topics.items():
|
|
81
|
+
docs.append(collect_docs(uri, func, "topic"))
|
|
82
|
+
|
|
83
|
+
async def get_schema(_: Invocation) -> Result:
|
|
84
|
+
return Result(args=docs)
|
|
85
|
+
|
|
86
|
+
options = RegisterOptions(invoke=InvokeOptions.ROUNDROBIN)
|
|
87
|
+
await session.register(app.schema_procedure, get_schema, options=options)
|
|
88
|
+
print(f"serving schema at procedure {app.schema_procedure}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@contextlib.asynccontextmanager
|
|
92
|
+
async def resolve_dependencies(meta: ProcedureMetadata) -> AsyncGenerator:
|
|
93
|
+
result = {}
|
|
94
|
+
|
|
95
|
+
for key, value in meta.async_dependencies.items():
|
|
96
|
+
result[key] = await value()
|
|
97
|
+
|
|
98
|
+
async with contextlib.AsyncExitStack() as stack:
|
|
99
|
+
for name, dependency in meta.async_ctx_dependencies.items():
|
|
100
|
+
result[name] = await stack.enter_async_context(dependency())
|
|
101
|
+
|
|
102
|
+
yield result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def register_async(session: AsyncSession, uri: str, func: callable):
|
|
106
|
+
if not inspect.iscoroutinefunction(func):
|
|
107
|
+
raise RuntimeError(f"function {func.__name__} for procedure '{uri}' must be a coroutine")
|
|
108
|
+
|
|
109
|
+
meta = _validate_procedure_function(func, uri)
|
|
110
|
+
|
|
111
|
+
async def _handle_invocation(invocation: Invocation) -> Result:
|
|
112
|
+
ensure_caller_allowed(invocation.details, meta.allowed_roles)
|
|
113
|
+
validate_invocation_parameters(invocation, meta)
|
|
114
|
+
details = assemble_call_details(uri, meta, invocation)
|
|
115
|
+
|
|
116
|
+
if meta.dynamic_model:
|
|
117
|
+
kwargs = _sanitize_incoming_data(invocation.args, invocation.kwargs, meta.request_args)
|
|
118
|
+
handle_model_validation(meta.request_model, **kwargs)
|
|
119
|
+
|
|
120
|
+
async with resolve_dependencies(meta) as deps:
|
|
121
|
+
result = await func(**kwargs, **deps, **details)
|
|
122
|
+
|
|
123
|
+
return _handle_result(result, meta.response_model, meta.response_args)
|
|
124
|
+
elif meta.request_model is not None:
|
|
125
|
+
kwargs = _sanitize_incoming_data(invocation.args, invocation.kwargs, meta.request_args)
|
|
126
|
+
model = handle_model_validation(meta.request_model, **kwargs, **details)
|
|
127
|
+
input_data = {meta.positional_field_name: model}
|
|
128
|
+
|
|
129
|
+
async with resolve_dependencies(meta) as deps:
|
|
130
|
+
result = await func(**input_data, **deps, **details)
|
|
131
|
+
|
|
132
|
+
return _handle_result(result, meta.response_model, meta.response_args)
|
|
133
|
+
elif meta.no_args:
|
|
134
|
+
async with resolve_dependencies(meta) as deps:
|
|
135
|
+
result = await func(**deps, **details)
|
|
136
|
+
|
|
137
|
+
return _handle_result(result, meta.response_model, meta.response_args)
|
|
138
|
+
else:
|
|
139
|
+
async with resolve_dependencies(meta) as deps:
|
|
140
|
+
input_data = {meta.positional_field_name: invocation}
|
|
141
|
+
result = await func(**input_data, **deps, **details)
|
|
142
|
+
|
|
143
|
+
return _handle_result(result, meta.response_model, meta.response_args)
|
|
144
|
+
|
|
145
|
+
await session.register(uri, _handle_invocation, options=meta.register_options)
|
|
146
|
+
print(f"Registered procedure {uri}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def subscribe_async(session: AsyncSession, topic: str, func: callable):
|
|
150
|
+
if not inspect.iscoroutinefunction(func):
|
|
151
|
+
raise RuntimeError(f"function {func.__name__} for topic '{topic}' must be a coroutine")
|
|
152
|
+
|
|
153
|
+
meta = _validate_topic_function(func, topic)
|
|
154
|
+
|
|
155
|
+
async def _handle_event(event: Event) -> None:
|
|
156
|
+
details = assemble_event_details(topic, meta, event)
|
|
157
|
+
validate_event_parameters(event, meta)
|
|
158
|
+
|
|
159
|
+
if meta.dynamic_model:
|
|
160
|
+
kwargs = _sanitize_incoming_data(event.args, event.kwargs, meta.request_args)
|
|
161
|
+
handle_model_validation(meta.request_model, **kwargs)
|
|
162
|
+
|
|
163
|
+
async with resolve_dependencies(meta) as deps:
|
|
164
|
+
await func(**kwargs, **deps, **details)
|
|
165
|
+
|
|
166
|
+
elif meta.request_model is not None:
|
|
167
|
+
kwargs = _sanitize_incoming_data(event.args, event.kwargs, meta.request_args)
|
|
168
|
+
model = handle_model_validation(meta.request_model, **kwargs)
|
|
169
|
+
|
|
170
|
+
async with resolve_dependencies(meta) as deps:
|
|
171
|
+
input_data = {meta.positional_field_name: model}
|
|
172
|
+
await func(**input_data, **deps, **details)
|
|
173
|
+
|
|
174
|
+
elif meta.no_args:
|
|
175
|
+
async with resolve_dependencies(meta) as deps:
|
|
176
|
+
await func(**deps, **details)
|
|
177
|
+
else:
|
|
178
|
+
async with resolve_dependencies(meta) as deps:
|
|
179
|
+
input_data = {meta.positional_field_name: event}
|
|
180
|
+
await func(**input_data, **deps, **details)
|
|
181
|
+
|
|
182
|
+
await session.subscribe(topic, _handle_event, options=meta.subscribe_options)
|
|
183
|
+
print(f"Subscribed topic {topic}")
|
xconn/_client/cli.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from xconn import App, run
|
|
8
|
+
from xconn._client import helpers
|
|
9
|
+
from xconn.types import WebsocketConfig
|
|
10
|
+
from xconn._client.sync import connect_sync
|
|
11
|
+
from xconn._client.async_ import connect_async
|
|
12
|
+
from xconn._client.types import ClientConfig, CommandArgs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handle_start(command_args: CommandArgs):
|
|
16
|
+
if command_args.no_config:
|
|
17
|
+
helpers.validate_auth_inputs(command_args.private_key, command_args.ticket, command_args.secret)
|
|
18
|
+
config = ClientConfig(
|
|
19
|
+
url=command_args.url,
|
|
20
|
+
realm=command_args.realm,
|
|
21
|
+
authid=command_args.authid,
|
|
22
|
+
secret=command_args.secret,
|
|
23
|
+
ticket=command_args.ticket,
|
|
24
|
+
private_key=command_args.private_key,
|
|
25
|
+
)
|
|
26
|
+
config.websocket_config = WebsocketConfig(
|
|
27
|
+
command_args.open_timeout, command_args.ping_interval, command_args.ping_timeout
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
else:
|
|
31
|
+
config_path = os.path.join(command_args.directory, "xapp.yaml")
|
|
32
|
+
if not os.path.exists(config_path):
|
|
33
|
+
print("xapp.yaml not found, initialize a client first")
|
|
34
|
+
exit(1)
|
|
35
|
+
|
|
36
|
+
flags = (
|
|
37
|
+
command_args.url,
|
|
38
|
+
command_args.realm,
|
|
39
|
+
command_args.authid,
|
|
40
|
+
command_args.secret,
|
|
41
|
+
command_args.ticket,
|
|
42
|
+
)
|
|
43
|
+
if any(flag is not None for flag in flags):
|
|
44
|
+
raise RuntimeError("Use either config file OR the individual flags, not both")
|
|
45
|
+
|
|
46
|
+
with open(config_path) as f:
|
|
47
|
+
config_raw = yaml.safe_load(f)
|
|
48
|
+
|
|
49
|
+
config = ClientConfig(**config_raw)
|
|
50
|
+
helpers.validate_auth_inputs(command_args.private_key, command_args.ticket, command_args.secret)
|
|
51
|
+
|
|
52
|
+
config.authmethod = helpers.select_authmethod(config)
|
|
53
|
+
|
|
54
|
+
split = command_args.app.split(":")
|
|
55
|
+
if len(split) != 2:
|
|
56
|
+
raise RuntimeError("invalid app argument, must be of format: module:instance")
|
|
57
|
+
|
|
58
|
+
sys.path.append(command_args.directory)
|
|
59
|
+
module = importlib.import_module(split[0])
|
|
60
|
+
app: App = getattr(module, split[1])
|
|
61
|
+
if not isinstance(app, App):
|
|
62
|
+
raise RuntimeError(f"app instance is of unknown type {type(app)}")
|
|
63
|
+
|
|
64
|
+
if command_args.asyncio:
|
|
65
|
+
run(connect_async(app, config, start_router=command_args.start_router))
|
|
66
|
+
else:
|
|
67
|
+
connect_sync(app, config, start_router=command_args.start_router)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def handle_init(
|
|
71
|
+
url: str,
|
|
72
|
+
realm: str,
|
|
73
|
+
authid: str,
|
|
74
|
+
authmethod: str,
|
|
75
|
+
secret: str,
|
|
76
|
+
open_timeout: int,
|
|
77
|
+
ping_interval: int,
|
|
78
|
+
ping_timeout: int,
|
|
79
|
+
):
|
|
80
|
+
if os.path.exists("xapp.yaml"):
|
|
81
|
+
print("xapp.yaml already exists")
|
|
82
|
+
exit(1)
|
|
83
|
+
|
|
84
|
+
if os.path.exists("xapp.py"):
|
|
85
|
+
print("xapp.py already exists")
|
|
86
|
+
exit(1)
|
|
87
|
+
|
|
88
|
+
with open("xapp.yaml", "w") as f:
|
|
89
|
+
f.write(
|
|
90
|
+
yaml.dump(
|
|
91
|
+
{
|
|
92
|
+
"url": url,
|
|
93
|
+
"realm": realm,
|
|
94
|
+
"authid": authid,
|
|
95
|
+
"authmethod": authmethod,
|
|
96
|
+
"secret": secret,
|
|
97
|
+
"websocket_config": {
|
|
98
|
+
"open_timeout": open_timeout,
|
|
99
|
+
"ping_interval": ping_interval,
|
|
100
|
+
"ping_timeout": ping_timeout,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
with open("sample.py", "w") as f:
|
|
107
|
+
f.write("""from xconn import App
|
|
108
|
+
|
|
109
|
+
app = App()
|
|
110
|
+
|
|
111
|
+
@app.register("io.xconn.hello")
|
|
112
|
+
async def my_procedure(first_name: str, last_name: str, age: int):
|
|
113
|
+
print(first_name + " " + last_name + " " + str(age))
|
|
114
|
+
return first_name, last_name, age
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.subscribe("io.xconn.publish")
|
|
118
|
+
async def my_topic():
|
|
119
|
+
print("received event...")
|
|
120
|
+
""")
|
|
121
|
+
|
|
122
|
+
print("XConn App initialized.")
|
|
123
|
+
print("The config is xapp.yaml and sample app is sample.py. Run below command to start the sample")
|
|
124
|
+
print("")
|
|
125
|
+
print("xapp start sample:app --asyncio --start-router")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def handle_stop(directory: str):
|
|
129
|
+
# TODO: when the app starts create a xapp.pid with the pid of the process
|
|
130
|
+
# when stop is called just request the OS to kill that PID.
|
|
131
|
+
print(directory)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def add_client_subparser(subparsers):
|
|
135
|
+
start = subparsers.add_parser("start", help="Start XConn App")
|
|
136
|
+
start.add_argument("APP", type=str)
|
|
137
|
+
start.add_argument("--url", type=str)
|
|
138
|
+
start.add_argument("--realm", type=str)
|
|
139
|
+
start.add_argument("--directory", type=str, default=".")
|
|
140
|
+
start.add_argument("--asyncio", action="store_true", default=False)
|
|
141
|
+
start.add_argument("--start-router", action="store_true", default=False)
|
|
142
|
+
start.add_argument("--authid", type=str)
|
|
143
|
+
start.add_argument("--secret", type=str)
|
|
144
|
+
start.add_argument("--ticket", type=str)
|
|
145
|
+
start.add_argument("--private-key", type=str)
|
|
146
|
+
start.add_argument("--no-config", action="store_true", default=False)
|
|
147
|
+
start.add_argument("--open-timeout", type=int, default=10)
|
|
148
|
+
start.add_argument("--ping-interval", type=int, default=20)
|
|
149
|
+
start.add_argument("--ping-timeout", type=int, default=20)
|
|
150
|
+
start.set_defaults(func=lambda args: handle_start(CommandArgs(**vars(args))))
|
|
151
|
+
|
|
152
|
+
stop = subparsers.add_parser("stop", help="Stop a running XConn App")
|
|
153
|
+
stop.add_argument("--directory", type=str, default=".")
|
|
154
|
+
stop.set_defaults(func=lambda args: handle_stop(args.directory))
|
|
155
|
+
|
|
156
|
+
init = subparsers.add_parser("init", help="Init a new XConn App")
|
|
157
|
+
init.add_argument("--url", type=str, default="ws://127.0.0.1:8080/ws")
|
|
158
|
+
init.add_argument("--realm", type=str, default="realm1")
|
|
159
|
+
init.add_argument("--authid", type=str, default="anonymous")
|
|
160
|
+
init.add_argument("--authmethod", type=str, default="anonymous")
|
|
161
|
+
init.add_argument("--secret", type=str, default="")
|
|
162
|
+
init.add_argument("--open-timeout", type=int, default=10)
|
|
163
|
+
init.add_argument("--ping-interval", type=int, default=20)
|
|
164
|
+
init.add_argument("--ping-timeout", type=int, default=20)
|
|
165
|
+
init.set_defaults(
|
|
166
|
+
func=lambda args: handle_init(
|
|
167
|
+
args.url,
|
|
168
|
+
args.realm,
|
|
169
|
+
args.authid,
|
|
170
|
+
args.authmethod,
|
|
171
|
+
args.secret,
|
|
172
|
+
args.open_timeout,
|
|
173
|
+
args.ping_interval,
|
|
174
|
+
args.ping_timeout,
|
|
175
|
+
)
|
|
176
|
+
)
|