runtimepy 5.5.0__py3-none-any.whl → 5.6.1__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.
runtimepy/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # =====================================
2
2
  # generator=datazen
3
3
  # version=3.1.4
4
- # hash=e8348ec684fac751916510d1d8c7dafb
4
+ # hash=d200504a923864c56b7eb211fd10233a
5
5
  # =====================================
6
6
 
7
7
  """
@@ -10,7 +10,7 @@ Useful defaults and other package metadata.
10
10
 
11
11
  DESCRIPTION = "A framework for implementing Python services."
12
12
  PKG_NAME = "runtimepy"
13
- VERSION = "5.5.0"
13
+ VERSION = "5.6.1"
14
14
 
15
15
  # runtimepy-specific content.
16
16
  METRICS_NAME = "metrics"
@@ -14,6 +14,9 @@ from vcorelib.args import CommandFunction as _CommandFunction
14
14
  from runtimepy.commands.arbiter import arbiter_cmd
15
15
  from runtimepy.commands.common import FACTORIES, arbiter_args, cmd_with_jit
16
16
 
17
+ SSL_PASSTHROUGH = ["cafile", "capath", "cadata", "certfile"]
18
+ PASSTHROUGH = SSL_PASSTHROUGH + ["keyfile"]
19
+
17
20
 
18
21
  def port_name(args: _Namespace, port: str = "port") -> str:
19
22
  """Get the name for a connection factory's port."""
@@ -25,7 +28,7 @@ def server_data(args: _Namespace) -> dict[str, Any]:
25
28
 
26
29
  return {
27
30
  "factory": args.factory,
28
- "kwargs": {"port": f"${port_name(args)}", "host": args.host},
31
+ "kwargs": get_kwargs(args, port=f"${port_name(args)}", host=args.host),
29
32
  }
30
33
 
31
34
 
@@ -34,6 +37,26 @@ def is_websocket(args: _Namespace) -> bool:
34
37
  return "websocket" in args.factory.lower()
35
38
 
36
39
 
40
+ def is_ssl(kwargs: dict[str, Any]) -> bool:
41
+ """Determine if server arugments indicate SSL use."""
42
+ return any(x in kwargs for x in SSL_PASSTHROUGH)
43
+
44
+
45
+ def get_kwargs(args: _Namespace, **kwargs) -> dict[str, Any]:
46
+ """Get boilerplate kwargs."""
47
+
48
+ new_kwargs: dict[str, Any] = {**kwargs}
49
+
50
+ # Pass additional arguments through.
51
+ print(args)
52
+ for opt in PASSTHROUGH:
53
+ value = getattr(args, opt, None)
54
+ if value is not None:
55
+ new_kwargs[opt] = value
56
+
57
+ return new_kwargs
58
+
59
+
37
60
  def client_data(args: _Namespace) -> dict[str, Any]:
38
61
  """Get client data based on command-line arguments."""
39
62
 
@@ -43,7 +66,9 @@ def client_data(args: _Namespace) -> dict[str, Any]:
43
66
  kwargs: dict[str, Any] = {}
44
67
 
45
68
  if is_websocket(args):
46
- arg_list.append(f"ws://localhost:{port}")
69
+ arg_list.append(
70
+ f"ws{'s' if is_ssl(get_kwargs(args)) else ''}://localhost:{port}"
71
+ )
47
72
  elif not args.udp:
48
73
  kwargs["host"] = "localhost"
49
74
  kwargs["port"] = port
@@ -76,7 +101,9 @@ def config_data(args: _Namespace) -> dict[str, Any]:
76
101
  {
77
102
  "name": port_name(args, port="server"),
78
103
  "factory": args.factory,
79
- "kwargs": {"local_addr": ["0.0.0.0", f"${port_name(args)}"]},
104
+ "kwargs": get_kwargs(
105
+ args, local_addr=["0.0.0.0", f"${port_name(args)}"]
106
+ ),
80
107
  }
81
108
  )
82
109
 
@@ -108,6 +135,11 @@ def add_server_cmd(parser: _ArgumentParser) -> _CommandFunction:
108
135
  """Add server-command arguments to its parser."""
109
136
 
110
137
  with arbiter_args(parser, nargs="*"):
138
+ for optional in PASSTHROUGH:
139
+ parser.add_argument(
140
+ f"--{optional}", help="passed directly to instantiation"
141
+ )
142
+
111
143
  parser.add_argument(
112
144
  "--host",
113
145
  default="0.0.0.0",
@@ -53,6 +53,12 @@ class App {
53
53
  this.switchTab(hash.tab);
54
54
  }
55
55
 
56
+ /* Handle settings controls. */
57
+ loadSettings();
58
+
59
+ /* Handle individual settings. */
60
+ this.handleInitialMinTxPeriod();
61
+
56
62
  startMainLoop();
57
63
  }
58
64
  }, {once : true});
@@ -62,6 +68,33 @@ class App {
62
68
 
63
69
  bootstrap_init();
64
70
  }
71
+
72
+ handleInitialMinTxPeriod() {
73
+ if ("min-tx-period-ms" in settings) {
74
+ /* Set up event handle. */
75
+ setupCursorContext(settings["min-tx-period-ms"], (elem, down, move,
76
+ up) => {
77
+ setupCursorMove(
78
+ elem, down, move, up,
79
+ (event) => { this.updateMinTxPeriod(Number(event.target.value)); });
80
+ });
81
+
82
+ /* Set slider to correct value. */
83
+ settings["min-tx-period-ms"].value = hash.minTxPeriod;
84
+ settings["min-tx-period-ms"].title = hash.minTxPeriod;
85
+ }
86
+
87
+ this.updateMinTxPeriod();
88
+ }
89
+
90
+ updateMinTxPeriod(value) {
91
+ if (value || value === 0) {
92
+ hash.setMinTxPeriod(value);
93
+ settings["min-tx-period-ms"].title = value;
94
+ }
95
+ this.worker.postMessage(
96
+ {"event" : {"worker" : {"min-tx-period-ms" : hash.minTxPeriod}}});
97
+ }
65
98
  }
66
99
 
67
100
  function startMainLoop() {
@@ -6,6 +6,8 @@ class PlotModalManager {
6
6
  this.body = this.container.querySelector(".modal-body");
7
7
  this.footer = this.container.querySelector(".modal-footer");
8
8
 
9
+ this.plotStatus = this.container.querySelector("#plot-status-inner");
10
+
9
11
  this.byEnv = {};
10
12
  }
11
13
 
@@ -34,7 +36,7 @@ class PlotModalManager {
34
36
  }
35
37
  }
36
38
 
37
- this.body.innerHTML = content;
39
+ this.plotStatus.innerHTML = content;
38
40
  }
39
41
  }
40
42
 
@@ -9,6 +9,7 @@ class WindowHashManager {
9
9
  this.channelsShown = true;
10
10
  this.plotChannels = {};
11
11
  this.filters = {};
12
+ this.minTxPeriod = 0.0;
12
13
  }
13
14
 
14
15
  tabClick(event) {
@@ -160,8 +161,13 @@ class WindowHashManager {
160
161
  for (let item of split) {
161
162
  if (item.includes("=")) {
162
163
  let keyVal = item.split("=");
163
- if (keyVal.length == 2 && keyVal[0] == "filter" && keyVal[1]) {
164
- this.updateTabFilter(keyVal[1]);
164
+ if (keyVal.length == 2) {
165
+ if (keyVal[0] == "filter" && keyVal[1]) {
166
+ this.updateTabFilter(keyVal[1]);
167
+ }
168
+ if (keyVal[0] == "min-tx-period-ms") {
169
+ this.minTxPeriod = Number(keyVal[1]);
170
+ }
165
171
  }
166
172
  }
167
173
  }
@@ -176,6 +182,11 @@ class WindowHashManager {
176
182
  return result;
177
183
  }
178
184
 
185
+ setMinTxPeriod(value) {
186
+ this.minTxPeriod = value;
187
+ this.update();
188
+ }
189
+
179
190
  buildHash() {
180
191
  let hash = this.tab;
181
192
 
@@ -189,6 +200,10 @@ class WindowHashManager {
189
200
  hash += ",hide-channels"
190
201
  }
191
202
 
203
+ if (this.minTxPeriod != 0.0) {
204
+ hash += `,min-tx-period-ms=${this.minTxPeriod}`;
205
+ }
206
+
192
207
  for (let tab in tabs) {
193
208
  let firstChan = true;
194
209
 
runtimepy/data/js/init.js CHANGED
@@ -46,3 +46,15 @@ function setupCursorMove(elem, down, move, up, handleMove) {
46
46
  function randomHexColor() {
47
47
  return "#" + (Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, "0");
48
48
  }
49
+
50
+ /* Load settings control mappings. */
51
+ let settings = {};
52
+
53
+ function loadSettings() {
54
+ for (const key of ["min-tx-period-ms"]) {
55
+ let elem = document.getElementById(`setting-${key}`);
56
+ if (elem) {
57
+ settings[key] = elem;
58
+ }
59
+ }
60
+ }
runtimepy/data/js/util.js CHANGED
@@ -1,6 +1,15 @@
1
1
  function worker_config(config) {
2
2
  let worker_cfg = {};
3
3
 
4
+ let port_name = "runtimepy_websocket";
5
+ let uri_prefix = "ws";
6
+
7
+ /* Ensured TLS is handled properly. */
8
+ if (location.protocol.includes("https")) {
9
+ port_name = "runtimepy_secure_websocket";
10
+ uri_prefix += "s";
11
+ }
12
+
4
13
  /* Look for connections to establish. */
5
14
  let ports = config["config"]["ports"];
6
15
  for (let port_idx in ports) {
@@ -10,11 +19,11 @@ function worker_config(config) {
10
19
  let hostname = window.location.hostname;
11
20
 
12
21
  /* This business logic could use some work. */
13
- if (port["name"].includes("runtimepy_websocket")) {
22
+ if (port["name"].includes(port_name)) {
14
23
  if (port["name"].includes("data")) {
15
- worker_cfg["data"] = "ws://" + hostname + ":" + port["port"];
24
+ worker_cfg["data"] = `${uri_prefix}://${hostname}:` + port["port"];
16
25
  } else {
17
- worker_cfg["json"] = "ws://" + hostname + ":" + port["port"];
26
+ worker_cfg["json"] = `${uri_prefix}://${hostname}:` + port["port"];
18
27
  }
19
28
  }
20
29
  }
@@ -13,11 +13,16 @@ function create_connections(config) {
13
13
 
14
14
  const plots = new PlotManager();
15
15
 
16
+ /* Used to control messaging rate with server. */
17
+ let minTxPeriod = 0.0;
18
+
16
19
  async function message(data) {
17
20
  if ("plot" in data) {
18
21
  /* Forward the 'name' field. */
19
22
  data["plot"]["name"] = data["name"];
20
23
  await plots.handleMessage(data["plot"]);
24
+ } else if ("min-tx-period-ms" in data) {
25
+ minTxPeriod = data["min-tx-period-ms"];
21
26
  } else {
22
27
  console.log(`Message for worker:`);
23
28
  console.log(data);
@@ -38,9 +43,14 @@ async function start(config) {
38
43
  onmessage = async (event) => {
39
44
  /* Handle messages meant for this thread. */
40
45
  if ("event" in event.data && "worker" in event.data["event"]) {
46
+ let data = event.data["event"]["worker"];
47
+
41
48
  /* Forward the 'name' field. */
42
- event.data["event"]["worker"]["name"] = event.data["name"];
43
- await message(event.data["event"]["worker"]);
49
+ if ("name" in event.data) {
50
+ data["name"] = event.data["name"];
51
+ }
52
+
53
+ await message(data);
44
54
  }
45
55
  /* Forward all other messages to the server. */
46
56
  else {
@@ -63,13 +73,18 @@ async function start(config) {
63
73
  /* Tell main thread we're ready to go. */
64
74
  postMessage(0);
65
75
 
76
+ let messageTxTime = 0.0;
77
+
66
78
  /* Set up the main request-animation-frame loop. */
67
79
  function render(time) {
68
80
  /* Render plot. */
69
81
  plots.render(time);
70
82
 
71
83
  /* Keep the server synchronized with frames. */
72
- conns["json"].send_json({"ui" : {"time" : time}});
84
+ if (messageTxTime + minTxPeriod <= time) {
85
+ conns["json"].send_json({"ui" : {"time" : time}});
86
+ messageTxTime = time;
87
+ }
73
88
 
74
89
  requestAnimationFrame(render);
75
90
  }
@@ -42,6 +42,7 @@ class TcpConnectionFactory(_ConnectionFactory, _Generic[T]):
42
42
  """Create a task that will run a connection server."""
43
43
 
44
44
  assert not [*args], "Only keyword arguments are used!"
45
+
45
46
  return self.kind.app(
46
47
  stop_sig,
47
48
  manager=manager,
@@ -27,26 +27,27 @@ async def launch_browser(app: AppInfo) -> None:
27
27
  """
28
28
 
29
29
  # Launch browser based on config option.
30
- if config_param(app, "xdg_open_http", False):
31
-
32
- port: Any
33
- for port in app.config["root"]["ports"]: # type: ignore
34
- if "http_server" in port["name"]:
35
- # URI parameters.
36
- hostname = config_param(app, "xdg_host", "localhost")
37
-
38
- # Assemble URI.
39
- uri = f"http://{hostname}:{port['port']}/"
40
-
41
- # Add a fragment if one was specified.
42
- fragment = config_param(app, "xdg_fragment", "")
43
- if fragment:
44
- uri += "#" + fragment
45
-
46
- with suppress(FileNotFoundError):
47
- await app.stack.enter_async_context(
48
- spawn_exec("xdg-open", uri)
49
- )
30
+ for prefix in ["http", "https"]:
31
+ if config_param(app, f"xdg_open_{prefix}", False):
32
+
33
+ port: Any
34
+ for port in app.config["root"]["ports"]: # type: ignore
35
+ if f"{prefix}_server" in port["name"]:
36
+ # URI parameters.
37
+ hostname = config_param(app, "xdg_host", "localhost")
38
+
39
+ # Assemble URI.
40
+ uri = f"{prefix}://{hostname}:{port['port']}/"
41
+
42
+ # Add a fragment if one was specified.
43
+ fragment = config_param(app, "xdg_fragment", "")
44
+ if fragment:
45
+ uri += "#" + fragment
46
+
47
+ with suppress(FileNotFoundError):
48
+ await app.stack.enter_async_context(
49
+ spawn_exec("xdg-open", uri)
50
+ )
50
51
 
51
52
 
52
53
  # Could add an interface for adding multiple applications.
@@ -11,6 +11,7 @@ from runtimepy.net.arbiter.info import AppInfo
11
11
  from runtimepy.net.server.app.bootstrap.elements import input_box
12
12
  from runtimepy.net.server.app.bootstrap.tabs import TabbedContent
13
13
  from runtimepy.net.server.app.env.modal import Modal
14
+ from runtimepy.net.server.app.env.settings import plot_settings
14
15
  from runtimepy.net.server.app.env.tab import ChannelEnvironmentTab
15
16
  from runtimepy.net.server.app.placeholder import dummy_tabs, under_construction
16
17
  from runtimepy.net.server.app.sound import SoundTab
@@ -85,8 +86,7 @@ def channel_environments(app: AppInfo, tabs: TabbedContent) -> None:
85
86
  )
86
87
 
87
88
  # Plot settings modal.
88
- plot_settings = Modal(tabs, name="plot", icon="graph-up")
89
- under_construction(plot_settings.footer)
89
+ plot_settings(tabs)
90
90
 
91
91
  # Experimental features.
92
92
  if app.config_param("experimental", False):
@@ -0,0 +1,59 @@
1
+ """
2
+ A module implementing an application-settings modal.
3
+ """
4
+
5
+ # third-party
6
+ from svgen.element.html import div
7
+
8
+ # internal
9
+ from runtimepy.net.server.app.bootstrap.elements import flex, slider
10
+ from runtimepy.net.server.app.bootstrap.tabs import TabbedContent
11
+ from runtimepy.net.server.app.env.modal import Modal
12
+ from runtimepy.net.server.app.placeholder import under_construction
13
+
14
+
15
+ def plot_settings(tabs: TabbedContent) -> None:
16
+ """Create the plot settings modal."""
17
+
18
+ modal = Modal(tabs, name="plot", icon="graph-up")
19
+ under_construction(modal.footer)
20
+
21
+ div(tag="h1", text="settings", parent=modal.body)
22
+ div(tag="hr", parent=modal.body)
23
+
24
+ div(tag="h2", text="plot status", parent=modal.body)
25
+ div(id="plot-status-inner", parent=modal.body)
26
+ div(tag="hr", parent=modal.body)
27
+
28
+ div(tag="h2", text="minimum transmit period (ms)", parent=modal.body)
29
+
30
+ div(
31
+ tag="p",
32
+ text=(
33
+ "Can be used to throttle the rate of "
34
+ "client <-> server communication. Use the 'ui' tab's metrics to "
35
+ "determine performance impact. Note that only this browser tab's "
36
+ "messaging rate can be controlled (not other connected clients')."
37
+ ),
38
+ parent=modal.body,
39
+ )
40
+
41
+ container = flex(parent=modal.body)
42
+
43
+ div(
44
+ text="0 ms ('high', run at native refresh rate)",
45
+ parent=container,
46
+ class_str="text-nowrap text-primary",
47
+ )
48
+
49
+ slider(
50
+ 0, 100, 100, parent=container, value=0, id="setting-min-tx-period-ms"
51
+ ).add_class("ms-3 me-3")
52
+
53
+ div(
54
+ text="100 ms ('low', 10 Hz)",
55
+ parent=container,
56
+ class_str="text-nowrap text-primary",
57
+ )
58
+
59
+ div(tag="hr", parent=modal.body)
runtimepy/net/ssl.py ADDED
@@ -0,0 +1,33 @@
1
+ """
2
+ A module implementing SSL-related interfaces.
3
+ """
4
+
5
+ # built-in
6
+ import ssl
7
+ from typing import Any
8
+
9
+
10
+ def handle_possible_ssl(client: bool = True, **kwargs) -> dict[str, Any]:
11
+ """Handle creating an SSL context based on keyword arguments."""
12
+
13
+ args = ["cafile", "capath", "cadata"]
14
+ if (
15
+ kwargs.pop("use_ssl", False)
16
+ or any(x in kwargs for x in args)
17
+ or "certfile" in kwargs
18
+ ):
19
+ context = ssl.create_default_context(
20
+ purpose=(
21
+ ssl.Purpose.SERVER_AUTH if client else ssl.Purpose.CLIENT_AUTH
22
+ ),
23
+ **{x: kwargs.pop(x, None) for x in args},
24
+ )
25
+
26
+ if "certfile" in kwargs:
27
+ context.load_cert_chain(
28
+ kwargs.pop("certfile"), keyfile=kwargs.pop("keyfile", None)
29
+ )
30
+
31
+ kwargs["ssl"] = context
32
+
33
+ return kwargs
@@ -27,6 +27,7 @@ from runtimepy.net.connection import EchoConnection as _EchoConnection
27
27
  from runtimepy.net.connection import NullConnection as _NullConnection
28
28
  from runtimepy.net.manager import ConnectionManager as _ConnectionManager
29
29
  from runtimepy.net.mixin import TransportMixin as _TransportMixin
30
+ from runtimepy.net.ssl import handle_possible_ssl
30
31
  from runtimepy.net.tcp.create import (
31
32
  TcpTransportProtocol,
32
33
  tcp_transport_protocol_backoff,
@@ -70,6 +71,20 @@ class TcpConnection(_Connection, _TransportMixin):
70
71
  self._protocol = protocol
71
72
  self._protocol.conn = self
72
73
 
74
+ @classmethod
75
+ def get_log_prefix(cls, is_ssl: bool = False) -> str:
76
+ """Get a logging prefix for this instance."""
77
+
78
+ # Default implementation doesn't handle this.
79
+ del is_ssl
80
+
81
+ return cls.log_prefix
82
+
83
+ @property
84
+ def is_ssl(self) -> bool:
85
+ """Determine if this connection uses SSL."""
86
+ return self._transport.get_extra_info("sslcontext") is not None
87
+
73
88
  async def _await_message(self) -> _Optional[_Union[_BinaryMessage, str]]:
74
89
  """Await the next message. Return None on error or failure."""
75
90
 
@@ -142,20 +157,22 @@ class TcpConnection(_Connection, _TransportMixin):
142
157
  callback(self.conn)
143
158
 
144
159
  eloop = _get_event_loop()
160
+
161
+ server_kwargs = handle_possible_ssl(client=False, **kwargs)
162
+ is_ssl = "ssl" in server_kwargs
145
163
  server = await eloop.create_server(
146
- CallbackProtocol, family=_socket.AF_INET, **kwargs
164
+ CallbackProtocol,
165
+ family=_socket.AF_INET,
166
+ **server_kwargs,
147
167
  )
148
168
  async with server:
149
169
  for socket in server.sockets:
150
- sockname = socket.getsockname()
151
170
  LOG.info(
152
- "Started %s server listening on '%s%s' (%s%s:%d).",
171
+ "Started %s%s server listening on '%s%s'.",
172
+ "secure " if is_ssl else "",
153
173
  cls.log_alias,
154
- cls.log_prefix,
174
+ cls.get_log_prefix(is_ssl=is_ssl),
155
175
  _sockname(socket),
156
- cls.log_prefix,
157
- sockname[0] if sockname[0] != "0.0.0.0" else "localhost",
158
- sockname[1],
159
176
  )
160
177
  yield server
161
178
 
@@ -193,7 +210,10 @@ class TcpConnection(_Connection, _TransportMixin):
193
210
  @classmethod
194
211
  @_asynccontextmanager
195
212
  async def create_pair(
196
- cls: type[T], peer: type[V] = None
213
+ cls: type[T],
214
+ peer: type[V] = None,
215
+ serve_kwargs: dict[str, _Any] = None,
216
+ connect_kwargs: dict[str, _Any] = None,
197
217
  ) -> _AsyncIterator[tuple[V, T]]:
198
218
  """Create a connection pair."""
199
219
 
@@ -212,13 +232,20 @@ class TcpConnection(_Connection, _TransportMixin):
212
232
  peer = cls # type: ignore
213
233
  assert peer is not None
214
234
 
235
+ if serve_kwargs is None:
236
+ serve_kwargs = {}
237
+
215
238
  server = await stack.enter_async_context(
216
- peer.serve(callback, port=0, backlog=1)
239
+ peer.serve(callback, port=0, backlog=1, **serve_kwargs)
217
240
  )
218
241
 
219
242
  host = server.sockets[0].getsockname()
243
+
244
+ if connect_kwargs is None:
245
+ connect_kwargs = {}
246
+
220
247
  client = await cls.create_connection(
221
- host="localhost", port=host[1]
248
+ host="localhost", port=host[1], **connect_kwargs
222
249
  )
223
250
  await cond.acquire()
224
251
 
@@ -11,6 +11,7 @@ from typing import Callable, Optional
11
11
 
12
12
  # internal
13
13
  from runtimepy.net.backoff import ExponentialBackoff
14
+ from runtimepy.net.ssl import handle_possible_ssl
14
15
  from runtimepy.net.tcp.protocol import QueueProtocol
15
16
  from runtimepy.net.util import try_log_connection_error
16
17
 
@@ -26,7 +27,7 @@ async def tcp_transport_protocol(**kwargs) -> TcpTransportProtocol:
26
27
 
27
28
  transport: _Transport
28
29
  transport, protocol = await _asyncio.get_event_loop().create_connection(
29
- QueueProtocol, **kwargs
30
+ QueueProtocol, **handle_possible_ssl(**kwargs)
30
31
  )
31
32
  return transport, protocol
32
33
 
@@ -58,7 +58,6 @@ class HttpConnection(_TcpConnection):
58
58
  expecting_response: bool
59
59
 
60
60
  log_alias = "HTTP"
61
- log_prefix = "http://"
62
61
 
63
62
  # Handlers registered at the class level so that instances created at
64
63
  # runtime don't need additional initialization.
@@ -79,6 +78,12 @@ class HttpConnection(_TcpConnection):
79
78
  self.handlers[http.HTTPMethod.GET] = self.get_handler
80
79
  self.handlers[http.HTTPMethod.POST] = self.post_handler
81
80
 
81
+ @classmethod
82
+ def get_log_prefix(cls, is_ssl: bool = False) -> str:
83
+ """Get a logging prefix for this instance."""
84
+
85
+ return f"http{'s' if is_ssl else ''}://"
86
+
82
87
  async def get_handler(
83
88
  self,
84
89
  response: ResponseHeader,
runtimepy/net/util.py CHANGED
@@ -134,15 +134,24 @@ def normalize_host(
134
134
  return IPv6Host(*args) # type: ignore
135
135
 
136
136
 
137
+ USE_FQDN = {"::", "0.0.0.0"}
138
+
139
+
137
140
  @cache
138
141
  def hostname(ip_address: str) -> str:
139
142
  """
140
143
  Attempt to get a string hostname for a string IP address argument that
141
144
  'gethostbyaddr' accepts. Otherwise return the original string
142
145
  """
146
+
143
147
  result = ip_address
144
- with _suppress(_socket.herror, OSError):
145
- result = _socket.gethostbyaddr(ip_address)[0]
148
+
149
+ if ip_address in USE_FQDN:
150
+ result = _socket.getfqdn()
151
+ else:
152
+ with _suppress(_socket.herror, OSError):
153
+ result = _socket.gethostbyaddr(ip_address)[0]
154
+
146
155
  return result
147
156
 
148
157
 
@@ -10,6 +10,7 @@ from contextlib import AsyncExitStack as _AsyncExitStack
10
10
  from contextlib import asynccontextmanager as _asynccontextmanager
11
11
  from contextlib import suppress as _suppress
12
12
  from logging import getLogger as _getLogger
13
+ from typing import Any as _Any
13
14
  from typing import AsyncIterator as _AsyncIterator
14
15
  from typing import Awaitable as _Awaitable
15
16
  from typing import Callable as _Callable
@@ -36,6 +37,7 @@ from runtimepy.net.connection import BinaryMessage, Connection
36
37
  from runtimepy.net.connection import EchoConnection as _EchoConnection
37
38
  from runtimepy.net.connection import NullConnection as _NullConnection
38
39
  from runtimepy.net.manager import ConnectionManager as _ConnectionManager
40
+ from runtimepy.net.ssl import handle_possible_ssl
39
41
 
40
42
  T = _TypeVar("T", bound="WebsocketConnection")
41
43
  ConnectionInit = _Callable[[T], _Awaitable[bool]]
@@ -93,7 +95,11 @@ class WebsocketConnection(Connection):
93
95
  async def create_connection(cls: type[T], uri: str, **kwargs) -> T:
94
96
  """Connect a client to an endpoint."""
95
97
 
96
- protocol = await getattr(websockets, "connect")(uri, **kwargs)
98
+ kwargs.setdefault("use_ssl", uri.startswith("wss"))
99
+
100
+ protocol = await getattr(websockets, "connect")(
101
+ uri, **handle_possible_ssl(**kwargs)
102
+ )
97
103
  return cls(protocol)
98
104
 
99
105
  @classmethod
@@ -101,7 +107,11 @@ class WebsocketConnection(Connection):
101
107
  async def client(cls: type[T], uri: str, **kwargs) -> _AsyncIterator[T]:
102
108
  """A wrapper for connecting a client."""
103
109
 
104
- async with getattr(websockets, "connect")(uri, **kwargs) as protocol:
110
+ kwargs.setdefault("use_ssl", uri.startswith("wss"))
111
+
112
+ async with getattr(websockets, "connect")(
113
+ uri, **handle_possible_ssl(**kwargs)
114
+ ) as protocol:
105
115
  yield cls(protocol)
106
116
 
107
117
  @classmethod
@@ -154,7 +164,9 @@ class WebsocketConnection(Connection):
154
164
 
155
165
  @classmethod
156
166
  @_asynccontextmanager
157
- async def create_pair(cls: type[T]) -> _AsyncIterator[tuple[T, T]]:
167
+ async def create_pair(
168
+ cls: type[T], serve_kwargs: dict[str, _Any] = None
169
+ ) -> _AsyncIterator[tuple[T, T]]:
158
170
  """Obtain a connected pair of WebsocketConnection objects."""
159
171
 
160
172
  server_conn: _Optional[T] = None
@@ -167,15 +179,21 @@ class WebsocketConnection(Connection):
167
179
  return True
168
180
 
169
181
  async with _AsyncExitStack() as stack:
182
+ if serve_kwargs is None:
183
+ serve_kwargs = {}
184
+
185
+ serve_kwargs = handle_possible_ssl(client=False, **serve_kwargs)
186
+ is_ssl = "ssl" in serve_kwargs
187
+
170
188
  # Start a server.
171
189
  server = await stack.enter_async_context(
172
- _serve(server_init, host="0.0.0.0", port=0)
190
+ _serve(server_init, host="0.0.0.0", port=0, **serve_kwargs)
173
191
  )
174
192
 
175
193
  host = list(server.sockets)[0].getsockname()
176
194
 
177
195
  client_conn = await stack.enter_async_context(
178
- cls.client(f"ws://localhost:{host[1]}")
196
+ cls.client(f"ws{'s' if is_ssl else ''}://localhost:{host[1]}")
179
197
  )
180
198
 
181
199
  # Connect a client and yield both sides of the connection.
@@ -193,13 +211,17 @@ class WebsocketConnection(Connection):
193
211
  ) -> _AsyncIterator[_WebSocketServer]:
194
212
  """Serve a WebSocket server."""
195
213
 
214
+ kwargs = handle_possible_ssl(client=False, **kwargs)
215
+ is_ssl = "ssl" in kwargs
216
+
196
217
  async with _serve(
197
218
  cls.server_handler(init=init, stop_sig=stop_sig, manager=manager),
198
219
  **kwargs,
199
220
  ) as server:
200
221
  for socket in server.sockets:
201
222
  LOG.info(
202
- "Started WebSocket server listening on '%s'.",
223
+ "Started WebSocket server listening on 'ws%s://%s'.",
224
+ "s" if is_ssl else "",
203
225
  _sockname(socket),
204
226
  )
205
227
  yield server
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: runtimepy
3
- Version: 5.5.0
3
+ Version: 5.6.1
4
4
  Summary: A framework for implementing Python services.
5
5
  Home-page: https://github.com/vkottler/runtimepy
6
6
  Author: Vaughn Kottler
@@ -18,9 +18,9 @@ Requires-Python: >=3.11
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: psutil
21
- Requires-Dist: svgen >=0.6.8
22
- Requires-Dist: websockets
23
21
  Requires-Dist: vcorelib >=3.3.1
22
+ Requires-Dist: websockets
23
+ Requires-Dist: svgen >=0.6.8
24
24
  Provides-Extra: test
25
25
  Requires-Dist: pylint ; extra == 'test'
26
26
  Requires-Dist: flake8 ; extra == 'test'
@@ -44,11 +44,11 @@ Requires-Dist: uvloop ; (sys_platform != "win32" and sys_platform != "cygwin") a
44
44
  =====================================
45
45
  generator=datazen
46
46
  version=3.1.4
47
- hash=5c5edaf1332d842e9ca04a018455beba
47
+ hash=a82bbd8413b0036b345251da16b462c3
48
48
  =====================================
49
49
  -->
50
50
 
51
- # runtimepy ([5.5.0](https://pypi.org/project/runtimepy/))
51
+ # runtimepy ([5.6.1](https://pypi.org/project/runtimepy/))
52
52
 
53
53
  [![python](https://img.shields.io/pypi/pyversions/runtimepy.svg)](https://pypi.org/project/runtimepy/)
54
54
  ![Build Status](https://github.com/vkottler/runtimepy/workflows/Python%20Package/badge.svg)
@@ -165,8 +165,10 @@ options:
165
165
  ```
166
166
  $ ./venv3.12/bin/runtimepy server -h
167
167
 
168
- usage: runtimepy server [-h] [-i] [-w] [--no-poller] [--host HOST] [-p PORT]
169
- [-u] [-l]
168
+ usage: runtimepy server [-h] [-i] [-w] [--no-poller] [--cafile CAFILE]
169
+ [--capath CAPATH] [--cadata CADATA]
170
+ [--certfile CERTFILE] [--keyfile KEYFILE]
171
+ [--host HOST] [-p PORT] [-u] [-l]
170
172
  factory [configs ...]
171
173
 
172
174
  positional arguments:
@@ -181,6 +183,11 @@ options:
181
183
  ensure that a 'wait_for_stop' application method is
182
184
  run last
183
185
  --no-poller don't run a connection-metrics poller task
186
+ --cafile CAFILE passed directly to instantiation
187
+ --capath CAPATH passed directly to instantiation
188
+ --cadata CADATA passed directly to instantiation
189
+ --certfile CERTFILE passed directly to instantiation
190
+ --keyfile KEYFILE passed directly to instantiation
184
191
  --host HOST host address to listen on (default: '0.0.0.0')
185
192
  -p PORT, --port PORT port to listen on (default: 0)
186
193
  -u, --udp whether or not this is a UDP-based server (otherwise
@@ -1,4 +1,4 @@
1
- runtimepy/__init__.py,sha256=ShrPdHuQw62Pp-9QV8h8xgc2KyWFgJwfnR6O97tC7Ec,390
1
+ runtimepy/__init__.py,sha256=sAv_oOiBI0nH6iHhMNxClwYnWlOwwJSeNZcfXxevMiU,390
2
2
  runtimepy/__main__.py,sha256=OPAed6hggoQdw-6QAR62mqLC-rCkdDhOq0wyeS2vDRI,332
3
3
  runtimepy/app.py,sha256=sTvatbsGZ2Hdel36Si_WUbNMtg9CzsJyExr5xjIcxDE,970
4
4
  runtimepy/dev_requirements.txt,sha256=j0dh11ztJAzfaUL0iFheGjaZj9ppDzmTkclTT8YKO8c,230
@@ -33,7 +33,7 @@ runtimepy/commands/all.py,sha256=jH2dsmkqyBFe_2ZlPFpko0UCMW3fFfJsuGIbeJFbDoQ,161
33
33
  runtimepy/commands/arbiter.py,sha256=CtTMRYpqCAN3vWHkkr9jqWpoF7JGNXafKIBFmkarAfc,1567
34
34
  runtimepy/commands/common.py,sha256=NvZdeIFBHAF52c1n7vqD59DW6ywc-rG5iC5MpuhGf-c,2449
35
35
  runtimepy/commands/mtu.py,sha256=LFFjTU4SsuV3j7Mhx_WuKa5lfdfMm70zJvDWToVrP7E,1357
36
- runtimepy/commands/server.py,sha256=T5IwBeqwJPpg35Ms_Vmz6xS1T-8U3fcgiRU6mAFlkEU,3767
36
+ runtimepy/commands/server.py,sha256=G1q6bgq0HpsKvDFcPyAC--f3-OuZqtnR4FK7b36Rqv8,4671
37
37
  runtimepy/commands/task.py,sha256=6xRVlRwpEZVhrcY18sQcfdWEOxeQZLeOF-6UrUURtO4,1435
38
38
  runtimepy/commands/tftp.py,sha256=djFQzYDxy2jvUseHJ4fDR3CowPxQ-Tu0IJz1SwapXX0,2361
39
39
  runtimepy/commands/tui.py,sha256=9hWA3_YATibUUDTVQr7UnKzPTDVJ7WxWKTYYQpLoyrE,1869
@@ -56,11 +56,11 @@ runtimepy/data/js/DataConnection.js,sha256=DnX8FMehjJXqmI62UMYXSvl_XdfQMzq3XUDFb
56
56
  runtimepy/data/js/JsonConnection.js,sha256=rclZrbmWc_zSs6I_JhOgxnVPFIyPMo5WdjAe8alyZ3o,2729
57
57
  runtimepy/data/js/audio.js,sha256=bLkBqbeHMiGGidfL3iXjmVoF9seK-ZeZ3kwgOrcpgk4,1092
58
58
  runtimepy/data/js/events.js,sha256=rgz3Q_8J6sfU_7Sa7fG1mZD0pQ4S3vwN2mqcvQfePkM,554
59
- runtimepy/data/js/init.js,sha256=q1SfcbC0RKr-a3z48PLC1usdAj9dh1_EMw5sJvvQKGs,1279
59
+ runtimepy/data/js/init.js,sha256=IeFqfab7CM2-Z4fIbyGaUD4M2orUT8uLwcVlleQqXzg,1522
60
60
  runtimepy/data/js/main.js,sha256=r0P_0xx5Czd1jfTjsB-tLfwhp4iPNoajlYC858u0ltc,211
61
- runtimepy/data/js/util.js,sha256=jGs9iMo9yLaZzHcefCTnritotH-ak2Vn1n3BoEhW0MU,1149
62
- runtimepy/data/js/worker.js,sha256=Yz9VEbE1I1gMrw-Zt7VB3RoII_xHWrS6_nhESEf5c6Q,2128
63
- runtimepy/data/js/classes/App.js,sha256=5dQ2_M_23WRfO3MtQ_3imy_oBNnGcS2uWY3g9PS6fJI,2392
61
+ runtimepy/data/js/util.js,sha256=ymYV3xenF3LZ5fw6ACXFnqHiNhFzf9uS7UUal_KsXr0,1376
62
+ runtimepy/data/js/worker.js,sha256=V9deGAynjvUr1D-WGi3wUW8rxoaNLvBvayMoLFZk3w0,2444
63
+ runtimepy/data/js/classes/App.js,sha256=nnY42Q3tlNzf8JZtuGKyxJZLLNMfResdww8svOQMC3U,3402
64
64
  runtimepy/data/js/classes/ChannelTable.js,sha256=V9g4_6N1i7ci7FkhP9eBd9ENbkSBusO5AvWuIEHUKk8,2634
65
65
  runtimepy/data/js/classes/DataConnection.js,sha256=DnX8FMehjJXqmI62UMYXSvl_XdfQMzq3XUDFbLu2GgI,98
66
66
  runtimepy/data/js/classes/JsonConnection.js,sha256=vgQ3bGvVU5dNXn_uwvH7HjOQm0PwUSx0239Vfi7h1vE,2858
@@ -68,13 +68,13 @@ runtimepy/data/js/classes/OverlayManager.js,sha256=9JCo1_O7Gqjfrim4StWW_6JsRIC8r
68
68
  runtimepy/data/js/classes/Plot.js,sha256=44DUxvmOe2cqKRjrk91Abo-myxWWFAFUDsZb05F4rrA,1979
69
69
  runtimepy/data/js/classes/PlotDrawer.js,sha256=DIRCwFt_lXSMB7GFrl6Y6y5Z74A291YUmmrXSNPuVhw,4830
70
70
  runtimepy/data/js/classes/PlotManager.js,sha256=tGnqFP_d5Z9Hn20OQfgu-h0DKM5zhD91whG0pF7_VFk,4973
71
- runtimepy/data/js/classes/PlotModalManager.js,sha256=N313qUvzoJa2TI_aQjdllWK0Ii365spmbaUO4kxZj6s,847
71
+ runtimepy/data/js/classes/PlotModalManager.js,sha256=lEbACLC5VzV-aSAb7G-WacmLLf_IRx7-pNJs9lL8bvY,928
72
72
  runtimepy/data/js/classes/PointBuffer.js,sha256=iVtq_q5gBaV9IVX55pHVjQdMJ4phJ6QTdAiM7EZrLRA,5061
73
73
  runtimepy/data/js/classes/PointManager.js,sha256=0lr2AReLdDNrY47UuOjjCRDPMiSRsOVggFHDPf8V-6Y,460
74
74
  runtimepy/data/js/classes/TabFilter.js,sha256=FygFFmWa1IRQ0DZlrOscdrkz6W3VkG87nHSud56kCzI,1185
75
75
  runtimepy/data/js/classes/TabInterface.js,sha256=i-dM_FpRsp_hJEh6CdtSY8hiTS6-KZx5dkOYXD_crh0,12084
76
76
  runtimepy/data/js/classes/UnitSystem.js,sha256=ys4OMabq47k_VvJpRItm82U0IequDvx3ysRJOQzDf94,906
77
- runtimepy/data/js/classes/WindowHashManager.js,sha256=TqRATCUv9D55LiatIhIMgFpVu7D8ZgVp5IpQlu4cHrE,5899
77
+ runtimepy/data/js/classes/WindowHashManager.js,sha256=aQcdUwWw-Ksp-ffywOlv64TdG6R-UrP4DVGLrGIi8QQ,6253
78
78
  runtimepy/data/js/classes/WorkerInterface.js,sha256=qARPW1CUDnHnVFVE8UjqKq74QfommCLwd6nisy-ayOw,346
79
79
  runtimepy/data/js/tab/env.js,sha256=MB79l3XyXKELWRqHcTnwWHiwdiceLHl1N_s-mS33pyU,22
80
80
  runtimepy/data/js/tab/sound.js,sha256=RSKp0AXM_zGOCsUvIT-BUjIzOE7Dp5NHiQG4fy7gBgY,1388
@@ -124,7 +124,8 @@ runtimepy/net/connection.py,sha256=lg_cAGCAdOqlh3SURJRKDQ5TraiG7sV0kA_U2FGGNVU,1
124
124
  runtimepy/net/manager.py,sha256=-M-ZSB9izay6HK1ytTayAYnSHYAz34dcwxaiNhC4lWg,4264
125
125
  runtimepy/net/mixin.py,sha256=5UlFK4lRrJ2O0nEUuScGbkYd4-El-RruFt_UcQR0aic,3039
126
126
  runtimepy/net/mtu.py,sha256=XnLXAFMsDxK1Lj5v_zgWaBrC3lNqf81DkbDc6hpMdmI,3495
127
- runtimepy/net/util.py,sha256=foXz7ZZDFpc7_vazzKT304Edgf7sOXj3NrDOY87kNsU,5779
127
+ runtimepy/net/ssl.py,sha256=dj9uECPKDT5k-5vlR5I3Z7Go3WWZhbaJ9nb0rC3kJvg,854
128
+ runtimepy/net/util.py,sha256=P6WnH4n8JJkEfKwepk1eP4lGPxWjqcFv0yL3N0mvtrw,5897
128
129
  runtimepy/net/apps/__init__.py,sha256=vjo7e19QXtJwe6V6B-QGvYiJveYobnYIfpkKZrnS17w,710
129
130
  runtimepy/net/arbiter/__init__.py,sha256=ptKF995rYKvkm4Mya92vA5QEDqcFq5NRD0IYGqZ6_do,740
130
131
  runtimepy/net/arbiter/base.py,sha256=WRbgavarmOx6caQJmfI03udZvNC7o298uOhOsN-lp2E,14658
@@ -143,7 +144,7 @@ runtimepy/net/arbiter/housekeeping/__init__.py,sha256=80vzksjCq1r9Kx25YeOKTJu2Ek
143
144
  runtimepy/net/arbiter/imports/__init__.py,sha256=bjBks4kdwtkzG8VjsNJewaxT4_QFhVGoZf3g6R3lrEs,4980
144
145
  runtimepy/net/arbiter/imports/util.py,sha256=Ltp5hHUkahiUfIWeeK9fTtGQb9UMJZPqfKquuibCV9M,1071
145
146
  runtimepy/net/arbiter/struct/__init__.py,sha256=Vr38dp2X0PZOrAbjKsZ9xZdQ1j3z92s4QuvRtYYVuNI,5990
146
- runtimepy/net/arbiter/tcp/__init__.py,sha256=wNJzoD_aN3sbWZXnAu3-fvb-MbbAtcl5cIG_bpXRmGc,1529
147
+ runtimepy/net/arbiter/tcp/__init__.py,sha256=djNm8il_9aLNpGsYResJlFmyIqx9XNLqVay-mYnn8vc,1530
147
148
  runtimepy/net/arbiter/tcp/json.py,sha256=W9a_OwBPmIoB2XZf4iuAIWQhMg2qA9xejBhGBdNCPnI,742
148
149
  runtimepy/net/factories/__init__.py,sha256=rPdBVpgzzQYF61w6efQrEre71yMPHd6kanBpMdOX-3c,4672
149
150
  runtimepy/net/http/__init__.py,sha256=4TjFp_ajAVcOEvwtjlF6mG-9EbEePqFZht-QpWIKVBo,1802
@@ -156,7 +157,7 @@ runtimepy/net/http/version.py,sha256=mp6rgIM7-VUVKLCA0Uw96CmBkL0ET860lDVVEewpZ7w
156
157
  runtimepy/net/server/__init__.py,sha256=J8gl91YltD8Wo2y_AXxaL6liLu3vomfzUz_nULa3e2Y,6707
157
158
  runtimepy/net/server/html.py,sha256=xaTGelH4zrwndQjU24kbCj9Yqu-D17nK5682P6xa-cU,1153
158
159
  runtimepy/net/server/json.py,sha256=RfNt7Gr4-X5DMinV1UeiWneTIJr0LO6BXo2GzE0C1PQ,2475
159
- runtimepy/net/server/app/__init__.py,sha256=-nVkFEZyPcEYsElV5a70qSwsAm-eSkyj3r0c4zE0f3g,2660
160
+ runtimepy/net/server/app/__init__.py,sha256=1qAC9E0mqD73sGXb3CBI7ync2WFfZkMNcYlfLSs5wt8,2775
160
161
  runtimepy/net/server/app/base.py,sha256=1KMEWCwDf3cducIbt9geTmMwugAMKl1Io2sBr6qajo4,2082
161
162
  runtimepy/net/server/app/create.py,sha256=N-g3kClBsG4pKOd9tx947rOq4sfgrH_FAMVfZacjhFA,2666
162
163
  runtimepy/net/server/app/elements.py,sha256=KJt9vWqkfvniJMiLOJN467JjPPrEqJYZXmDuY1JoY1g,455
@@ -169,8 +170,9 @@ runtimepy/net/server/app/tab.py,sha256=4jhw71LfA23wT9m9e5ofw-tBXYW6xky4tGwBOlGkO
169
170
  runtimepy/net/server/app/bootstrap/__init__.py,sha256=ONhwx68piWjsrf88FMpda84TWSPqgi-RZCBuWCci_ak,1444
170
171
  runtimepy/net/server/app/bootstrap/elements.py,sha256=uWcaVBdrZvL3_lNZ13jQ8AfPN5f4ye2feZN7ve9RDnM,3868
171
172
  runtimepy/net/server/app/bootstrap/tabs.py,sha256=0azUtpZTO80kjhX65vU3K5QfW4cwK7ULixqM8O0FgKQ,4179
172
- runtimepy/net/server/app/env/__init__.py,sha256=oZPKZ6aCjG6eSZ3fVs0Q8oJrUTnEUifG8ihuByGdFw8,3221
173
+ runtimepy/net/server/app/env/__init__.py,sha256=SbIgHg2KQtYSLnHPQzHpK1--f_xl4ASFVTvx-TIDJUw,3202
173
174
  runtimepy/net/server/app/env/modal.py,sha256=d7OdKVJfKXnOkDhvsF2nQmTfZHAdx70wbK2pGlNIcEQ,1753
175
+ runtimepy/net/server/app/env/settings.py,sha256=M0DFibrzF5-nxZ8udKMi503HiTZWIQNbzj_t9TWi34Q,1720
174
176
  runtimepy/net/server/app/env/widgets.py,sha256=_kNvPl7MXnZOiwTjoZiU2hfuSjkLnRUrORTVDi3w7Ls,5312
175
177
  runtimepy/net/server/app/env/tab/__init__.py,sha256=stTVKyHljLQWnnhxkWPwa7bLdZtjhiMFbiVFgbiYaFI,647
176
178
  runtimepy/net/server/app/env/tab/base.py,sha256=uBPpOeqI23341IezIQcGvJciPfIL2P5qQgbZ74aQRKA,991
@@ -185,10 +187,10 @@ runtimepy/net/stream/base.py,sha256=Dg4vcR0n9y2122AyJ-9W-jkEhNla_EHO-DqJJPfGD4k,
185
187
  runtimepy/net/stream/string.py,sha256=61mgserU3p6j5gAcK0oe0aKqL6vDh7NtgJvbPoiAUPM,784
186
188
  runtimepy/net/stream/json/__init__.py,sha256=h--C_9moW92TC_e097FRRXcg8GJ6VVbMLXl1cICknys,2508
187
189
  runtimepy/net/tcp/__init__.py,sha256=OOWohegpoioSTf8M7uDf-4EV1IDungz7-U19L_2yW4I,250
188
- runtimepy/net/tcp/connection.py,sha256=8PbsdWQ32NTFAorEBVcJ3pPa9SpM0Y1FXm-FoSZ0JGs,7849
189
- runtimepy/net/tcp/create.py,sha256=yYlMuvV9N5i5PgwXrK8brpKdrQjmLNPtQsdEtnIMfH8,1940
190
+ runtimepy/net/tcp/connection.py,sha256=izRVSZdrhkk21javEdJB2BJTPmiz87_DoUE1oKuQq58,8596
191
+ runtimepy/net/tcp/create.py,sha256=zZsRs5KYpO3bNGh-DwEOEzjUDE4ixj-UBHYgZ0GvC7c,2013
190
192
  runtimepy/net/tcp/protocol.py,sha256=vEnIX3gUX2nrw9ofT_e4KYU4VY2k4WP0WuOi4eE_OOQ,1444
191
- runtimepy/net/tcp/http/__init__.py,sha256=NeJi6Utmpc2m3D18DFMIlXfv3xymxX7OIzImTFTz4zI,5504
193
+ runtimepy/net/tcp/http/__init__.py,sha256=4ZSM-Fma_IpmDNCu5E3VBPRrVxOjPgKpKAS3uag1V1I,5657
192
194
  runtimepy/net/tcp/scpi/__init__.py,sha256=aWCWQfdeyfoU9bpOnOtyIQbT1swl4ergXLFn5kXAH28,2105
193
195
  runtimepy/net/tcp/telnet/__init__.py,sha256=96eJFb301I3H2ivDtGMQtDDw09Xm5NRvM9VEC-wjt8c,4768
194
196
  runtimepy/net/tcp/telnet/codes.py,sha256=1-yyRe-Kz_W7d6B0P3iT1AaSNR3_Twmn-MUjKCJJknY,3518
@@ -204,7 +206,7 @@ runtimepy/net/udp/tftp/endpoint.py,sha256=so60LdPTG66N5tdhHhiX7j_TBHvNOTi4JIgLcg
204
206
  runtimepy/net/udp/tftp/enums.py,sha256=06juMd__pJZsyL8zO8p3hRucnOratt1qtz9zcxzMg4s,1579
205
207
  runtimepy/net/udp/tftp/io.py,sha256=w6cnUt-T-Ma6Vg8BWoRbsNnIWUv0HTY4am6bcLWxNJs,803
206
208
  runtimepy/net/websocket/__init__.py,sha256=YjSmoxiigmsI_hcQw6nueX7bxhrRGerEERnPvgLVEVA,313
207
- runtimepy/net/websocket/connection.py,sha256=Q4TDXcEXrUv1IfNg1veMxag9AyvvYmuYsnzufYaWnD8,8274
209
+ runtimepy/net/websocket/connection.py,sha256=r0HUB5PFhWGmmarwEKp10CJ449Iwj6WOcMyhjzgCMpU,8996
208
210
  runtimepy/noise/__init__.py,sha256=EJM7h3t_z74wwrn6FAFQwYE2yUcOZQ1K1IQqOb8Z0AI,384
209
211
  runtimepy/primitives/__init__.py,sha256=nwWJH1e0KN2NsVwQ3wvRtUpl9s9Ap8Q32NNZLGol0wU,2323
210
212
  runtimepy/primitives/base.py,sha256=BaGPUTeVMnLnTPcpjqnS2lzPN74Pe5C0XaQdgrTfW7A,9185
@@ -261,9 +263,9 @@ runtimepy/tui/task.py,sha256=nUZo9fuOC-k1Wpqdzkv9v1tQirCI28fZVgcC13Ijvus,1093
261
263
  runtimepy/tui/channels/__init__.py,sha256=evDaiIn-YS9uGhdo8ZGtP9VK1ek6sr_P1nJ9JuSET0o,4536
262
264
  runtimepy/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
263
265
  runtimepy/ui/controls.py,sha256=yvT7h3thbYaitsakcIAJ90EwKzJ4b-jnc6p3UuVf_XE,1241
264
- runtimepy-5.5.0.dist-info/LICENSE,sha256=okYCYhGsx_BlzvFdoNVBVpw_Cfb4SOqHA_VAARml4Hc,1071
265
- runtimepy-5.5.0.dist-info/METADATA,sha256=2wXUkVGv7ALo-oNtbi-51eMTqiVpXklvc0_4eQP4ECY,8851
266
- runtimepy-5.5.0.dist-info/WHEEL,sha256=nCVcAvsfA9TDtwGwhYaRrlPhTLV9m-Ga6mdyDtuwK18,91
267
- runtimepy-5.5.0.dist-info/entry_points.txt,sha256=-btVBkYv7ybcopqZ_pRky-bEzu3vhbaG3W3Z7ERBiFE,51
268
- runtimepy-5.5.0.dist-info/top_level.txt,sha256=0jPmh6yqHyyJJDwEID-LpQly-9kQ3WRMjH7Lix8peLg,10
269
- runtimepy-5.5.0.dist-info/RECORD,,
266
+ runtimepy-5.6.1.dist-info/LICENSE,sha256=okYCYhGsx_BlzvFdoNVBVpw_Cfb4SOqHA_VAARml4Hc,1071
267
+ runtimepy-5.6.1.dist-info/METADATA,sha256=ttK3mwA-y9-Jg8ARP2LWXia7GVDB7K1OhIZM3fd3imo,9280
268
+ runtimepy-5.6.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
269
+ runtimepy-5.6.1.dist-info/entry_points.txt,sha256=-btVBkYv7ybcopqZ_pRky-bEzu3vhbaG3W3Z7ERBiFE,51
270
+ runtimepy-5.6.1.dist-info/top_level.txt,sha256=0jPmh6yqHyyJJDwEID-LpQly-9kQ3WRMjH7Lix8peLg,10
271
+ runtimepy-5.6.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (73.0.0)
2
+ Generator: setuptools (75.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5