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 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
@@ -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
+ )