runtimepy 5.4.0__py3-none-any.whl → 5.4.2__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=7736b31bf224c6245fbf71943f29dc63
4
+ # hash=e6970089f5f2935c496cb3e9bb06b774
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.4.0"
13
+ VERSION = "5.4.2"
14
14
 
15
15
  # runtimepy-specific content.
16
16
  METRICS_NAME = "metrics"
@@ -3,6 +3,7 @@ A module implementing a base channel environment.
3
3
  """
4
4
 
5
5
  # built-in
6
+ import math
6
7
  from typing import AsyncIterator as _AsyncIterator
7
8
  from typing import Iterable as _Iterable
8
9
  from typing import Optional as _Optional
@@ -175,10 +176,17 @@ class BaseChannelEnvironment(_NamespaceMixin, FinalizeMixin):
175
176
  def values(self, resolve_enum: bool = True) -> ValueMap:
176
177
  """Get a new dictionary of current channel values."""
177
178
 
178
- return {
179
- name: self.value(name, resolve_enum=resolve_enum)
180
- for name in self.channels.names.names
181
- }
179
+ result: ValueMap = {}
180
+ for name in self.channels.names.names:
181
+ value = self.value(name, resolve_enum=resolve_enum)
182
+
183
+ # Don't store NaN. Python will allow JSON encoding but e.g.
184
+ # browsers (and the JSON specification) don't support decoding
185
+ # NaN.
186
+ if isinstance(value, str) or not math.isnan(value):
187
+ result[name] = value
188
+
189
+ return result
182
190
 
183
191
  def value(
184
192
  self, key: _RegistryKey, resolve_enum: bool = True, scaled: bool = True
runtimepy/commands/all.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # =====================================
2
2
  # generator=datazen
3
3
  # version=3.1.4
4
- # hash=89bfb0e2f56effabf3cb8f70d6349c13
4
+ # hash=5162b7ebed95a36719dad0f72cd0872f
5
5
  # =====================================
6
6
 
7
7
  """
@@ -19,6 +19,7 @@ from vcorelib.args import CommandRegister as _CommandRegister
19
19
  from runtimepy.commands.arbiter import add_arbiter_cmd
20
20
  from runtimepy.commands.server import add_server_cmd
21
21
  from runtimepy.commands.task import add_task_cmd
22
+ from runtimepy.commands.tftp import add_tftp_cmd
22
23
  from runtimepy.commands.tui import add_tui_cmd
23
24
 
24
25
 
@@ -41,6 +42,11 @@ def commands() -> _List[_Tuple[str, str, _CommandRegister]]:
41
42
  "run a task from a specific task factory",
42
43
  add_task_cmd,
43
44
  ),
45
+ (
46
+ "tftp",
47
+ "perform a tftp interaction",
48
+ add_tftp_cmd,
49
+ ),
44
50
  (
45
51
  "tui",
46
52
  "run a terminal interface for the channel environment",
@@ -0,0 +1,91 @@
1
+ """
2
+ An entry-point for the 'tftp' command.
3
+ """
4
+
5
+ # built-in
6
+ import argparse
7
+ import asyncio
8
+ from pathlib import Path
9
+ from socket import getaddrinfo
10
+
11
+ from vcorelib.args import CommandFunction
12
+
13
+ # third-party
14
+ from vcorelib.asyncio import run_handle_stop
15
+
16
+ # internal
17
+ from runtimepy.net.udp.tftp import tftp_read, tftp_write
18
+ from runtimepy.net.udp.tftp.base import DEFAULT_TIMEOUT_S, REEMIT_PERIOD_S
19
+ from runtimepy.net.udp.tftp.enums import DEFAULT_MODE
20
+
21
+
22
+ def tftp_cmd(args: argparse.Namespace) -> int:
23
+ """Execute the tftp command."""
24
+
25
+ # Resolve hostname as early as possible.
26
+ addr = (getaddrinfo(args.host, None)[0][4][0], args.port)
27
+
28
+ stop_sig = asyncio.Event()
29
+ kwargs = {
30
+ "mode": args.mode,
31
+ "timeout_s": args.timeout,
32
+ "reemit_period_s": args.reemit,
33
+ "process_kwargs": {"stop_sig": stop_sig},
34
+ }
35
+
36
+ if not args.their_file:
37
+ args.their_file = str(args.our_file)
38
+
39
+ if args.operation == "read":
40
+ task = tftp_read(addr, args.our_file, args.their_file, **kwargs)
41
+ else:
42
+ task = tftp_write(addr, args.our_file, args.their_file, **kwargs)
43
+
44
+ return run_handle_stop(
45
+ stop_sig, task, enable_uvloop=not getattr(args, "no_uvloop", False)
46
+ )
47
+
48
+
49
+ def add_tftp_cmd(parser: argparse.ArgumentParser) -> CommandFunction:
50
+ """Add tftp-command arguments to its parser."""
51
+
52
+ parser.add_argument(
53
+ "-p",
54
+ "--port",
55
+ type=int,
56
+ default=69,
57
+ help="port to message (default: %(default)s)",
58
+ )
59
+
60
+ parser.add_argument(
61
+ "-m",
62
+ "--mode",
63
+ default=DEFAULT_MODE,
64
+ help="tftp mode to use (default: %(default)s)",
65
+ )
66
+
67
+ parser.add_argument(
68
+ "-t",
69
+ "--timeout",
70
+ type=float,
71
+ default=DEFAULT_TIMEOUT_S,
72
+ help="timeout for each step",
73
+ )
74
+ parser.add_argument(
75
+ "-r",
76
+ "--reemit",
77
+ type=float,
78
+ default=REEMIT_PERIOD_S,
79
+ help="transmit period for each step",
80
+ )
81
+
82
+ parser.add_argument(
83
+ "operation", choices=["read", "write"], help="action to perform"
84
+ )
85
+
86
+ parser.add_argument("host", help="host to message")
87
+
88
+ parser.add_argument("our_file", type=Path, help="path to our file")
89
+ parser.add_argument("their_file", nargs="?", help="path to their file")
90
+
91
+ return tftp_cmd
@@ -9,4 +9,4 @@ clients:
9
9
  - factory: tftp
10
10
  name: tftp_server
11
11
  kwargs:
12
- local_addr: [localhost, "$tftp_server"]
12
+ local_addr: [127.0.0.1, "$tftp_server"]
runtimepy/entry.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # =====================================
2
2
  # generator=datazen
3
3
  # version=3.1.4
4
- # hash=c2bc26deadfa7cc275e815f499693863
4
+ # hash=79c31d1280a6e97b5d326aecb758c597
5
5
  # =====================================
6
6
 
7
7
  """
@@ -10,13 +10,14 @@ This package's command-line entry-point (boilerplate).
10
10
 
11
11
  # built-in
12
12
  import argparse
13
+ from logging import getLogger
13
14
  import os
14
15
  from pathlib import Path
15
16
  import sys
16
17
  from typing import List
17
18
 
18
19
  # third-party
19
- from vcorelib.logging import init_logging, logging_args
20
+ from vcorelib.logging import init_logging, log_time, logging_args
20
21
 
21
22
  # internal
22
23
  from runtimepy import DESCRIPTION, VERSION
@@ -68,7 +69,8 @@ def main(argv: List[str] = None) -> int:
68
69
  os.chdir(args.dir)
69
70
 
70
71
  # run the application
71
- result = entry(args)
72
+ with log_time(getLogger(__name__), "Command"):
73
+ result = entry(args)
72
74
  except SystemExit as exc:
73
75
  result = 1
74
76
  if exc.code is not None and isinstance(exc.code, int):
@@ -55,13 +55,11 @@ class Connection(LoggerMixinLevelControl, ChannelEnvironmentMixin, _ABC):
55
55
  # this can set 'uses_text_tx_queue' to False to avoid scheduling a
56
56
  # task for it.
57
57
  self._text_messages: _asyncio.Queue[str] = _asyncio.Queue()
58
- self.tx_text_hwm: int = 0
59
58
 
60
59
  # A queue for out-going binary messages. Connections that don't use
61
60
  # this can set 'uses_binary_tx_queue' to False to avoid scheduling a
62
61
  # task for it.
63
62
  self._binary_messages: _asyncio.Queue[BinaryMessage] = _asyncio.Queue()
64
- self.tx_binary_hwm: int = 0
65
63
 
66
64
  # Tasks common to connection processing.
67
65
  self._tasks: list[_asyncio.Task[None]] = []
@@ -159,17 +157,11 @@ class Connection(LoggerMixinLevelControl, ChannelEnvironmentMixin, _ABC):
159
157
 
160
158
  def send_text(self, data: str) -> None:
161
159
  """Enqueue a text message to send."""
162
-
163
160
  self._text_messages.put_nowait(data)
164
- self.tx_text_hwm = max(self.tx_text_hwm, self._text_messages.qsize())
165
161
 
166
162
  def send_binary(self, data: BinaryMessage) -> None:
167
163
  """Enqueue a binary message tos end."""
168
-
169
164
  self._binary_messages.put_nowait(data)
170
- self.tx_binary_hwm = max(
171
- self.tx_binary_hwm, self._binary_messages.qsize()
172
- )
173
165
 
174
166
  @property
175
167
  def disabled(self) -> bool:
@@ -194,6 +186,7 @@ class Connection(LoggerMixinLevelControl, ChannelEnvironmentMixin, _ABC):
194
186
 
195
187
  if self._enabled:
196
188
  self.logger.info("Disabling connection: '%s'.", reason)
189
+ self.log_metrics(label=reason)
197
190
  self.disable_extra()
198
191
 
199
192
  # Cancel tasks.
runtimepy/net/mixin.py CHANGED
@@ -22,7 +22,6 @@ class BinaryMessageQueueMixin:
22
22
  def __init__(self) -> None:
23
23
  """Initialize this protocol."""
24
24
  self.queue: _asyncio.Queue[_BinaryMessage] = _asyncio.Queue()
25
- self.queue_hwm: int = 0
26
25
 
27
26
 
28
27
  class TransportMixin:
@@ -4,7 +4,7 @@ A module implementing a channel-environment tab message-handling interface.
4
4
 
5
5
  # built-in
6
6
  import logging
7
- from typing import Any, Callable, cast
7
+ from typing import Any, Callable
8
8
 
9
9
  # internal
10
10
  from runtimepy.channel import Channel
@@ -22,6 +22,8 @@ class ChannelEnvironmentTabMessaging(ChannelEnvironmentTabBase):
22
22
  """Register a channel's value-change callback."""
23
23
 
24
24
  chan = self.command.env.field_or_channel(name)
25
+ assert isinstance(chan, Channel) or chan is not None
26
+ prim = chan.raw
25
27
 
26
28
  def callback(_, __) -> None:
27
29
  """Emit a change event to the stream."""
@@ -29,14 +31,9 @@ class ChannelEnvironmentTabMessaging(ChannelEnvironmentTabBase):
29
31
  # Render enumerations etc. here instead of trying to do it
30
32
  # in the UI.
31
33
  state.points[name].append(
32
- (
33
- self.command.env.value(name),
34
- cast(Channel[Any], chan).raw.last_updated_ns,
35
- )
34
+ (self.command.env.value(name), prim.last_updated_ns)
36
35
  )
37
36
 
38
- assert isinstance(chan, Channel) or chan is not None
39
- prim = chan.raw
40
37
  state.primitives[name] = prim
41
38
  state.callbacks[name] = prim.register_callback(callback)
42
39
 
@@ -28,9 +28,7 @@ class QueueProtocol(_BinaryMessageQueueMixin, _Protocol):
28
28
 
29
29
  def data_received(self, data: _BinaryMessage) -> None:
30
30
  """Handle incoming data."""
31
-
32
31
  self.queue.put_nowait(data)
33
- self.queue_hwm = max(self.queue_hwm, self.queue.qsize())
34
32
 
35
33
  def connection_made(self, transport: _BaseTransport) -> None:
36
34
  """Log the connection establishment."""
@@ -126,14 +126,17 @@ class UdpConnection(_Connection, _TransportMixin):
126
126
  )
127
127
  return result is not None
128
128
 
129
+ should_connect: bool = True
130
+
129
131
  @classmethod
130
- async def create_connection(
131
- cls: type[T], connect: bool = True, **kwargs
132
- ) -> T:
132
+ async def create_connection(cls: type[T], **kwargs) -> T:
133
133
  """Create a UDP connection."""
134
134
 
135
135
  LOG.debug("kwargs: %s", kwargs)
136
136
 
137
+ # Allows certain connections to have more sane defaults.
138
+ connect = kwargs.pop("connect", cls.should_connect)
139
+
137
140
  # If the caller specifies a remote address but doesn't want a connected
138
141
  # socket, handle this after initial creation.
139
142
  remote_addr = None
@@ -173,8 +176,8 @@ class UdpConnection(_Connection, _TransportMixin):
173
176
  sock1.connect(("localhost", sock2.getsockname()[1]))
174
177
  sock2.connect(("localhost", sock1.getsockname()[1]))
175
178
 
176
- conn1 = await cls.create_connection(sock=sock1)
177
- conn2 = await cls.create_connection(sock=sock2)
179
+ conn1 = await cls.create_connection(sock=sock1, connect=True)
180
+ conn2 = await cls.create_connection(sock=sock2, connect=True)
178
181
  assert conn1.remote_address is not None
179
182
  assert conn2.remote_address is not None
180
183
 
@@ -29,15 +29,12 @@ class UdpQueueProtocol(_DatagramProtocol):
29
29
  self.queue: _asyncio.Queue[
30
30
  _Tuple[_BinaryMessage, _Tuple[str, int]]
31
31
  ] = _asyncio.Queue()
32
- self.queue_hwm: int = 0
33
32
 
34
33
  self.log_limiter = RateLimiter.from_s(1.0)
35
34
 
36
35
  def datagram_received(self, data: bytes, addr: _Tuple[str, int]) -> None:
37
36
  """Handle incoming data."""
38
-
39
37
  self.queue.put_nowait((data, addr))
40
- self.queue_hwm = max(self.queue_hwm, self.queue.qsize())
41
38
 
42
39
  def error_received(self, exc: Exception) -> None:
43
40
  """Log any received errors."""
@@ -17,7 +17,11 @@ from vcorelib.paths.info import FileInfo
17
17
 
18
18
  # internal
19
19
  from runtimepy.net import IpHost, normalize_host
20
- from runtimepy.net.udp.tftp.base import BaseTftpConnection
20
+ from runtimepy.net.udp.tftp.base import (
21
+ DEFAULT_TIMEOUT_S,
22
+ REEMIT_PERIOD_S,
23
+ BaseTftpConnection,
24
+ )
21
25
  from runtimepy.net.udp.tftp.enums import DEFAULT_MODE
22
26
  from runtimepy.util import PossiblePath, as_path
23
27
 
@@ -34,19 +38,19 @@ class TftpConnection(BaseTftpConnection):
34
38
  ) -> bool:
35
39
  """Request a tftp read operation."""
36
40
 
37
- endpoint = self.endpoint(addr)
38
41
  end_of_data = False
39
42
  idx = 1
40
43
 
41
- def ack_sender() -> None:
42
- """Send acks."""
43
- nonlocal idx
44
- self.send_ack(block=idx - 1, addr=addr)
45
-
46
44
  async with AsyncExitStack() as stack:
47
45
  # Claim read lock and ignore cancellation.
48
46
  stack.enter_context(suppress(asyncio.CancelledError))
49
- await stack.enter_async_context(endpoint.lock)
47
+
48
+ endpoint, event = await self._await_first_block(stack, addr=addr)
49
+
50
+ def ack_sender() -> None:
51
+ """Send acks."""
52
+ nonlocal idx
53
+ endpoint.ack_sender(idx - 1, endpoint.addr)
50
54
 
51
55
  def send_rrq() -> None:
52
56
  """Send request"""
@@ -56,13 +60,13 @@ class TftpConnection(BaseTftpConnection):
56
60
  "Requesting '%s' (%s) -> %s.", filename, mode, destination
57
61
  )
58
62
 
59
- event = asyncio.Event()
60
- endpoint.awaiting_blocks[idx] = event
61
-
62
63
  with self.log_time("Awaiting first data block", reminder=True):
63
64
  # Wait for first data block.
64
65
  if not await repeat_until(
65
- send_rrq, event, endpoint.period, endpoint.timeout
66
+ send_rrq,
67
+ event,
68
+ endpoint.period.value,
69
+ endpoint.timeout.value,
66
70
  ):
67
71
  endpoint.awaiting_blocks.pop(idx, None)
68
72
  self.logger.error("Didn't receive any data block.")
@@ -97,26 +101,30 @@ class TftpConnection(BaseTftpConnection):
97
101
  endpoint.awaiting_blocks[idx] = event
98
102
 
99
103
  success = await repeat_until(
100
- ack_sender, event, endpoint.period, endpoint.timeout
104
+ ack_sender,
105
+ event,
106
+ endpoint.period.value,
107
+ endpoint.timeout.value,
101
108
  )
102
109
  if success:
103
110
  write_block()
104
111
 
105
- # Repeat last ack in the background.
106
- if end_of_data:
107
- self._conn_tasks.append(
108
- asyncio.create_task(
109
- repeat_until( # type: ignore
110
- ack_sender,
111
- asyncio.Event(),
112
- endpoint.period,
113
- endpoint.timeout,
112
+ # Repeat last ack in the background.
113
+ if end_of_data:
114
+ self._conn_tasks.append(
115
+ asyncio.create_task(
116
+ repeat_until( # type: ignore
117
+ ack_sender,
118
+ asyncio.Event(),
119
+ endpoint.period.value,
120
+ endpoint.timeout.value,
121
+ )
114
122
  )
115
123
  )
116
- )
117
124
 
118
- # Make a to-string or log method for vcorelib FileInfo?
119
- #
125
+ # Ensure at least one ack sends.
126
+ await asyncio.sleep(0.01)
127
+
120
128
  self.logger.info(
121
129
  "Read %s (%s).",
122
130
  FileInfo.from_file(destination),
@@ -136,16 +144,14 @@ class TftpConnection(BaseTftpConnection):
136
144
  """Request a tftp write operation."""
137
145
 
138
146
  result = False
139
- endpoint = self.endpoint(addr)
140
147
 
141
148
  with as_path(source) as src:
142
149
  async with AsyncExitStack() as stack:
143
150
  # Claim write lock and ignore cancellation.
144
151
  stack.enter_context(suppress(asyncio.CancelledError))
145
- await stack.enter_async_context(endpoint.lock)
146
152
 
147
- event = asyncio.Event()
148
- endpoint.awaiting_acks[0] = event
153
+ # Set up first-ack handling.
154
+ endpoint, event = await self._await_first_ack(stack, addr=addr)
149
155
 
150
156
  def send_wrq() -> None:
151
157
  """Send request."""
@@ -154,7 +160,10 @@ class TftpConnection(BaseTftpConnection):
154
160
  # Wait for zeroeth ack.
155
161
  with self.log_time("Awaiting first ack", reminder=True):
156
162
  if not await repeat_until(
157
- send_wrq, event, endpoint.period, endpoint.timeout
163
+ send_wrq,
164
+ event,
165
+ endpoint.period.value,
166
+ endpoint.timeout.value,
158
167
  ):
159
168
  endpoint.awaiting_acks.pop(0, None)
160
169
  return result
@@ -170,6 +179,11 @@ class TftpConnection(BaseTftpConnection):
170
179
  )
171
180
 
172
181
  # Compare hashes.
182
+ self.logger.info(
183
+ "Reading '%s' %s.",
184
+ filename,
185
+ "succeeded" if result else "failed",
186
+ )
173
187
  if result:
174
188
  result = file_md5_hex(src) == file_md5_hex(tmp)
175
189
  self.logger.info(
@@ -185,6 +199,8 @@ async def tftp(
185
199
  addr: Union[IpHost, tuple[str, int]],
186
200
  process_kwargs: dict[str, Any] = None,
187
201
  connection_kwargs: dict[str, Any] = None,
202
+ timeout_s: float = DEFAULT_TIMEOUT_S,
203
+ reemit_period_s: float = REEMIT_PERIOD_S,
188
204
  ) -> AsyncIterator[TftpConnection]:
189
205
  """Use a tftp connection as a managed context."""
190
206
 
@@ -200,6 +216,9 @@ async def tftp(
200
216
  remote_addr=(addr.name, addr.port), **connection_kwargs
201
217
  )
202
218
  async with conn.process_then_disable(**process_kwargs):
219
+ # Set parameters.
220
+ conn.endpoint_timeout.value = timeout_s
221
+ conn.endpoint_period.value = reemit_period_s
203
222
  yield conn
204
223
 
205
224
 
@@ -211,6 +230,8 @@ async def tftp_write(
211
230
  verify: bool = True,
212
231
  process_kwargs: dict[str, Any] = None,
213
232
  connection_kwargs: dict[str, Any] = None,
233
+ timeout_s: float = DEFAULT_TIMEOUT_S,
234
+ reemit_period_s: float = REEMIT_PERIOD_S,
214
235
  ) -> bool:
215
236
  """Attempt to perform a tftp write."""
216
237
 
@@ -218,8 +239,9 @@ async def tftp_write(
218
239
  addr,
219
240
  process_kwargs=process_kwargs,
220
241
  connection_kwargs=connection_kwargs,
242
+ timeout_s=timeout_s,
243
+ reemit_period_s=reemit_period_s,
221
244
  ) as conn:
222
-
223
245
  # Perform tftp interaction.
224
246
  result = await conn.request_write(
225
247
  source, filename, mode=mode, addr=addr, verify=verify
@@ -235,6 +257,8 @@ async def tftp_read(
235
257
  mode: str = DEFAULT_MODE,
236
258
  process_kwargs: dict[str, Any] = None,
237
259
  connection_kwargs: dict[str, Any] = None,
260
+ timeout_s: float = DEFAULT_TIMEOUT_S,
261
+ reemit_period_s: float = REEMIT_PERIOD_S,
238
262
  ) -> bool:
239
263
  """Attempt to perform a tftp read."""
240
264
 
@@ -242,8 +266,9 @@ async def tftp_read(
242
266
  addr,
243
267
  process_kwargs=process_kwargs,
244
268
  connection_kwargs=connection_kwargs,
269
+ timeout_s=timeout_s,
270
+ reemit_period_s=reemit_period_s,
245
271
  ) as conn:
246
-
247
272
  result = await conn.request_read(
248
273
  destination, filename, mode=mode, addr=addr
249
274
  )
@@ -3,6 +3,8 @@ A module implementing a base tftp (RFC 1350) connection interface.
3
3
  """
4
4
 
5
5
  # built-in
6
+ import asyncio
7
+ from contextlib import AsyncExitStack
6
8
  from io import BytesIO
7
9
  import logging
8
10
  from pathlib import Path
@@ -22,7 +24,11 @@ from runtimepy.net.udp.tftp.enums import (
22
24
  encode_filename_mode,
23
25
  parse_filename_mode,
24
26
  )
25
- from runtimepy.primitives import Uint16
27
+ from runtimepy.net.util import normalize_host
28
+ from runtimepy.primitives import Double, Uint16
29
+
30
+ REEMIT_PERIOD_S = 0.20
31
+ DEFAULT_TIMEOUT_S = 1.0
26
32
 
27
33
 
28
34
  class BaseTftpConnection(UdpConnection):
@@ -33,6 +39,7 @@ class BaseTftpConnection(UdpConnection):
33
39
  _path: Path
34
40
 
35
41
  default_auto_restart = True
42
+ should_connect = False
36
43
 
37
44
  def set_root(self, path: Path) -> None:
38
45
  """Set a new root path for this instance."""
@@ -67,6 +74,21 @@ class BaseTftpConnection(UdpConnection):
67
74
  self.error_code = Uint16(time_source=metrics_time_ns)
68
75
  self.env.channel("error_code", self.error_code, enum="TftpErrorCode")
69
76
 
77
+ self.endpoint_period = Double(value=REEMIT_PERIOD_S)
78
+ self.env.channel(
79
+ "reemit_period_s",
80
+ self.endpoint_period,
81
+ commandable=True,
82
+ description="Time between packet re-transmissions.",
83
+ )
84
+ self.endpoint_timeout = Double(value=DEFAULT_TIMEOUT_S)
85
+ self.env.channel(
86
+ "timeout_s",
87
+ self.endpoint_timeout,
88
+ commandable=True,
89
+ description="A timeout used for each step.",
90
+ )
91
+
70
92
  # Message parsers.
71
93
  self.handlers = {
72
94
  TftpOpCode.RRQ.value: self._handle_rrq,
@@ -105,9 +127,39 @@ class BaseTftpConnection(UdpConnection):
105
127
 
106
128
  self.error_sender = error_sender
107
129
 
108
- self._endpoints: dict[str, TftpEndpoint] = {}
130
+ self._endpoints: dict[IpHost, TftpEndpoint] = {}
131
+ self._awaiting_first_ack: dict[str, TftpEndpoint] = {}
132
+ self._awaiting_first_block: dict[str, TftpEndpoint] = {}
109
133
  # self._self = self.endpoint(self.local_address)
110
134
 
135
+ async def _await_first_ack(
136
+ self,
137
+ stack: AsyncExitStack,
138
+ addr: Union[IpHost, tuple[str, int]] = None,
139
+ ) -> tuple[TftpEndpoint, asyncio.Event]:
140
+ """Set up an endpoint to wait for an initial ack from a server."""
141
+
142
+ endpoint = self.endpoint(addr)
143
+ await stack.enter_async_context(endpoint.lock)
144
+ event = asyncio.Event()
145
+ endpoint.awaiting_acks[0] = event
146
+ self._awaiting_first_ack[endpoint.addr.hostname] = endpoint
147
+ return endpoint, event
148
+
149
+ async def _await_first_block(
150
+ self,
151
+ stack: AsyncExitStack,
152
+ addr: Union[IpHost, tuple[str, int]] = None,
153
+ ) -> tuple[TftpEndpoint, asyncio.Event]:
154
+ """Set up an endpoint to wait for an initial block from a server."""
155
+
156
+ endpoint = self.endpoint(addr)
157
+ await stack.enter_async_context(endpoint.lock)
158
+ event = asyncio.Event()
159
+ endpoint.awaiting_blocks[1] = event
160
+ self._awaiting_first_block[endpoint.addr.hostname] = endpoint
161
+ return endpoint, event
162
+
111
163
  def endpoint(
112
164
  self, addr: Union[IpHost, tuple[str, int]] = None
113
165
  ) -> TftpEndpoint:
@@ -117,19 +169,21 @@ class BaseTftpConnection(UdpConnection):
117
169
  addr = self.remote_address
118
170
 
119
171
  assert addr is not None
120
- key = f"{addr[0]}:{addr[1]}"
172
+ addr = normalize_host(*addr)
121
173
 
122
- if key not in self._endpoints:
123
- self._endpoints[key] = TftpEndpoint(
174
+ if addr not in self._endpoints:
175
+ self._endpoints[addr] = TftpEndpoint(
124
176
  self._path,
125
177
  self.logger,
126
178
  addr,
127
179
  self.data_sender,
128
180
  self.ack_sender,
129
181
  self.error_sender,
182
+ self.endpoint_period,
183
+ self.endpoint_timeout,
130
184
  )
131
185
 
132
- return self._endpoints[key]
186
+ return self._endpoints[addr]
133
187
 
134
188
  def send_rrq(
135
189
  self,
@@ -249,15 +303,38 @@ class BaseTftpConnection(UdpConnection):
249
303
  ) -> None:
250
304
  """Handle a data message."""
251
305
 
306
+ endpoint = self.endpoint(addr)
252
307
  block = self._read_block_number(stream)
253
- self.endpoint(addr).handle_data(block, stream.read())
308
+
309
+ # Check if we're currently waiting for an initial block.
310
+ hostname = endpoint.addr.hostname
311
+ if block == 1 and hostname in self._awaiting_first_block:
312
+ to_update = self._awaiting_first_block[hostname]
313
+ del self._awaiting_first_block[hostname]
314
+ self._endpoints[endpoint.addr] = to_update
315
+ endpoint = to_update.update_from_other(endpoint)
316
+
317
+ endpoint.handle_data(block, stream.read())
254
318
 
255
319
  async def _handle_ack(
256
320
  self, stream: BinaryIO, addr: tuple[str, int]
257
321
  ) -> None:
258
322
  """Handle an acknowledge message."""
259
323
 
260
- self.endpoint(addr).handle_ack(self._read_block_number(stream))
324
+ endpoint = self.endpoint(addr)
325
+ block = self._read_block_number(stream)
326
+
327
+ # Check if we're currently waiting for an initial acknowledgement. This
328
+ # will come from the same host but a different port, so update
329
+ # references when this is detected.
330
+ hostname = endpoint.addr.hostname
331
+ if block == 0 and hostname in self._awaiting_first_ack:
332
+ to_update = self._awaiting_first_ack[hostname]
333
+ del self._awaiting_first_ack[hostname]
334
+ self._endpoints[endpoint.addr] = to_update
335
+ endpoint = to_update.update_from_other(endpoint)
336
+
337
+ endpoint.handle_ack(block)
261
338
 
262
339
  def _read_block_number(self, stream: BinaryIO) -> int:
263
340
  """Read block number from the stream."""
@@ -19,6 +19,7 @@ from vcorelib.paths.info import FileInfo
19
19
  from runtimepy.net import IpHost
20
20
  from runtimepy.net.udp.tftp.enums import TftpErrorCode
21
21
  from runtimepy.net.udp.tftp.io import tftp_chunks
22
+ from runtimepy.primitives import Double
22
23
 
23
24
  TftpDataSender = Callable[[int, bytes, Union[IpHost, tuple[str, int]]], None]
24
25
  TftpAckSender = Callable[[int, Union[IpHost, tuple[str, int]]], None]
@@ -39,10 +40,12 @@ class TftpEndpoint(LoggerMixin):
39
40
  self,
40
41
  root: Path,
41
42
  logger: LoggerType,
42
- addr: Union[IpHost, tuple[str, int]],
43
+ addr: IpHost,
43
44
  data_sender: TftpDataSender,
44
45
  ack_sender: TftpAckSender,
45
46
  error_sender: TftpErrorSender,
47
+ period: Double,
48
+ timeout: Double,
46
49
  ) -> None:
47
50
  """Initialize instance."""
48
51
 
@@ -68,10 +71,17 @@ class TftpEndpoint(LoggerMixin):
68
71
  self.max_block_size = TFTP_MAX_BLOCK
69
72
 
70
73
  # Runtime settings.
71
- self.period: float = 0.25
72
- self.timeout: float = 1.0
74
+ self.period = period
75
+ self.timeout = timeout
73
76
  self.log_limiter = RateLimiter.from_s(1.0)
74
77
 
78
+ def update_from_other(self, other: "TftpEndpoint") -> "TftpEndpoint":
79
+ """Update this endpoint's attributes with attributes of another's."""
80
+
81
+ self.logger.info("Updating address to '%s'.", other.addr)
82
+ self.addr = other.addr
83
+ return self
84
+
75
85
  def chunk_sender(self, block: int, data: bytes) -> Callable[[], None]:
76
86
  """Create a method that sends a specific block of data."""
77
87
 
@@ -132,7 +142,7 @@ class TftpEndpoint(LoggerMixin):
132
142
 
133
143
  def __str__(self) -> str:
134
144
  """Get this instance as a string."""
135
- return f"{self.addr[0]}:{self.addr[1]}"
145
+ return str(self.addr)
136
146
 
137
147
  def handle_error(self, error_code: TftpErrorCode, message: str) -> None:
138
148
  """Handle a tftp error message."""
@@ -164,8 +174,8 @@ class TftpEndpoint(LoggerMixin):
164
174
  # data.
165
175
  self._ack_sender(idx - 1),
166
176
  event,
167
- self.period,
168
- self.timeout,
177
+ self.period.value,
178
+ self.timeout.value,
169
179
  )
170
180
  and idx in self.blocks
171
181
  )
@@ -264,8 +274,8 @@ class TftpEndpoint(LoggerMixin):
264
274
  if not await repeat_until(
265
275
  self.chunk_sender(idx, chunk),
266
276
  event,
267
- self.period,
268
- self.timeout,
277
+ self.period.value,
278
+ self.timeout.value,
269
279
  ):
270
280
  success = False
271
281
  self.awaiting_acks.pop(idx, None)
runtimepy/net/util.py CHANGED
@@ -19,6 +19,11 @@ class IPv4Host(NamedTuple):
19
19
  name: str = ""
20
20
  port: int = 0
21
21
 
22
+ @property
23
+ def hostname(self) -> str:
24
+ """Get a hostname for this instance."""
25
+ return hostname(self.name)
26
+
22
27
  @property
23
28
  def address(self) -> ipaddress.IPv4Address:
24
29
  """Get an address object for this hostname."""
@@ -28,6 +33,10 @@ class IPv4Host(NamedTuple):
28
33
  """Get this host as a string."""
29
34
  return hostname_port(self.name, self.port)
30
35
 
36
+ def __hash__(self) -> int:
37
+ """Get a hash for this instance."""
38
+ return hash(str(self))
39
+
31
40
 
32
41
  class IPv6Host(NamedTuple):
33
42
  """See: https://docs.python.org/3/library/socket.html#socket-families."""
@@ -37,6 +46,11 @@ class IPv6Host(NamedTuple):
37
46
  flowinfo: int = 0
38
47
  scope_id: int = 0
39
48
 
49
+ @property
50
+ def hostname(self) -> str:
51
+ """Get a hostname for this instance."""
52
+ return hostname(self.name)
53
+
40
54
  @property
41
55
  def address(self) -> ipaddress.IPv6Address:
42
56
  """Get an address object for this hostname."""
@@ -46,6 +60,10 @@ class IPv6Host(NamedTuple):
46
60
  """Get this host as a string."""
47
61
  return hostname_port(self.name, self.port)
48
62
 
63
+ def __hash__(self) -> int:
64
+ """Get a hash for this instance."""
65
+ return hash(str(self))
66
+
49
67
 
50
68
  IpHost = _Union[IPv4Host, IPv6Host]
51
69
  IpHostlike = _Union[str, int, IpHost, None]
@@ -139,6 +139,20 @@ class Primitive(_Generic[T]):
139
139
  """Set a new underlying value."""
140
140
  self.set_value(value)
141
141
 
142
+ def _check_callbacks(self, curr: T, new: T) -> None:
143
+ """Determine if any callbacks should be serviced."""
144
+
145
+ if self.callbacks and curr != new:
146
+ to_remove = []
147
+ for ident, (callback, once) in self.callbacks.items():
148
+ callback(curr, new)
149
+ if once:
150
+ to_remove.append(ident)
151
+
152
+ # Remove one-time callbacks.
153
+ for item in to_remove:
154
+ self.remove_callback(item)
155
+
142
156
  def set_value(self, value: T, timestamp_ns: int = None) -> None:
143
157
  """Set a new underlying value."""
144
158
 
@@ -149,18 +163,7 @@ class Primitive(_Generic[T]):
149
163
 
150
164
  curr: T = self.raw.value # type: ignore
151
165
  self.raw.value = value
152
-
153
- # Call callbacks if the value has changed.
154
- if self.callbacks and curr != value:
155
- to_remove = []
156
- for ident, (callback, once) in self.callbacks.items():
157
- callback(curr, value)
158
- if once:
159
- to_remove.append(ident)
160
-
161
- # Remove one-time callbacks.
162
- for item in to_remove:
163
- self.remove_callback(item)
166
+ self._check_callbacks(curr, value)
164
167
 
165
168
  @property
166
169
  def scaled(self) -> Numeric:
@@ -2,6 +2,9 @@
2
2
  A module implementing a floating-point primitive interface.
3
3
  """
4
4
 
5
+ # built-in
6
+ import math
7
+
5
8
  # internal
6
9
  from runtimepy.primitives.evaluation import (
7
10
  EvalResult,
@@ -24,6 +27,13 @@ class BaseFloatPrimitive(PrimitiveIsCloseMixin[float]):
24
27
  """Initialize this floating-point primitive."""
25
28
  super().__init__(value=value, scaling=scaling, **kwargs)
26
29
 
30
+ def _check_callbacks(self, curr: float, new: float) -> None:
31
+ """Determine if any callbacks should be serviced."""
32
+
33
+ # Useless to provide NaN to callbacks.
34
+ if not math.isnan(new):
35
+ super()._check_callbacks(curr, new)
36
+
27
37
  async def wait_for_value(
28
38
  self,
29
39
  value: float,
@@ -134,7 +134,11 @@ class Serializable(ABC):
134
134
  """Assign a next serializable."""
135
135
 
136
136
  assert self.chain is None, self.chain
137
- self.chain = chain
137
+
138
+ # mypy regression?
139
+ self.chain = chain # type: ignore
140
+ assert self.chain is not None
141
+
138
142
  return self.chain.size
139
143
 
140
144
  def add_to_end(self, chain: T, array_length: int = None) -> int:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: runtimepy
3
- Version: 5.4.0
3
+ Version: 5.4.2
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: svgen >=0.6.8
21
+ Requires-Dist: vcorelib >=3.3.1
21
22
  Requires-Dist: websockets
22
23
  Requires-Dist: psutil
23
- Requires-Dist: vcorelib >=3.3.1
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=0ee282dadd7cdb7ebdc78b6c281116e4
47
+ hash=4f8a71a6066638ed1a90f375188f0578
48
48
  =====================================
49
49
  -->
50
50
 
51
- # runtimepy ([5.4.0](https://pypi.org/project/runtimepy/))
51
+ # runtimepy ([5.4.2](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)
@@ -90,7 +90,7 @@ This package is tested on the following platforms:
90
90
  $ ./venv3.12/bin/runtimepy -h
91
91
 
92
92
  usage: runtimepy [-h] [--version] [-v] [-q] [--curses] [--no-uvloop] [-C DIR]
93
- {arbiter,server,task,tui,noop} ...
93
+ {arbiter,server,task,tftp,tui,noop} ...
94
94
 
95
95
  A framework for implementing Python services.
96
96
 
@@ -104,11 +104,12 @@ options:
104
104
  -C DIR, --dir DIR execute from a specific directory
105
105
 
106
106
  commands:
107
- {arbiter,server,task,tui,noop}
107
+ {arbiter,server,task,tftp,tui,noop}
108
108
  set of available commands
109
109
  arbiter run a connection-arbiter application from a config
110
110
  server run a server for a specific connection factory
111
111
  task run a task from a specific task factory
112
+ tftp perform a tftp interaction
112
113
  tui run a terminal interface for the channel environment
113
114
  noop command stub (does nothing)
114
115
 
@@ -190,6 +191,31 @@ options:
190
191
 
191
192
  ```
192
193
 
194
+ ### `tftp`
195
+
196
+ ```
197
+ $ ./venv3.12/bin/runtimepy tftp -h
198
+
199
+ usage: runtimepy tftp [-h] [-p PORT] [-m MODE] [-t TIMEOUT] [-r REEMIT]
200
+ {read,write} host our_file [their_file]
201
+
202
+ positional arguments:
203
+ {read,write} action to perform
204
+ host host to message
205
+ our_file path to our file
206
+ their_file path to their file
207
+
208
+ options:
209
+ -h, --help show this help message and exit
210
+ -p PORT, --port PORT port to message (default: 69)
211
+ -m MODE, --mode MODE tftp mode to use (default: octet)
212
+ -t TIMEOUT, --timeout TIMEOUT
213
+ timeout for each step
214
+ -r REEMIT, --reemit REEMIT
215
+ transmit period for each step
216
+
217
+ ```
218
+
193
219
  ### `tui`
194
220
 
195
221
  ```
@@ -1,8 +1,8 @@
1
- runtimepy/__init__.py,sha256=5wUpQuutLgqlg-Y1kdL40QKqd_PY_BezYNzWxuDiuSo,390
1
+ runtimepy/__init__.py,sha256=ojyvRCtIcczCpJWeruS4_U9LFJ6NMGfeWnb-MKrQ3pU,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
5
- runtimepy/entry.py,sha256=j-IQzh-ZIQz4KgTBHcUwrVghfh9AZqCnPo2LpNd0rOw,1855
5
+ runtimepy/entry.py,sha256=3672ccoslf2h8Wg5M_SuW6SoEx0oslRoi0ngZsgjNz8,1954
6
6
  runtimepy/mapping.py,sha256=nb3btKUGHhuhkhPg9NnpBdh4QGQVjE0Zsu4Sl3OBIgY,5091
7
7
  runtimepy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  runtimepy/requirements.txt,sha256=Wf3CEWVylp7Em7-FMWWhjyAwxNvNmh7vPRqW2KkbbXY,115
@@ -12,7 +12,7 @@ runtimepy/channel/__init__.py,sha256=pf0RJ5g37_FVV8xoUNgzFGuIfbZEYSBA_cQlJSDTPDo
12
12
  runtimepy/channel/registry.py,sha256=nk36qM_Bf6qK6AFR0plaZHR1PU7b4LZqbQ0feJqk4lc,4784
13
13
  runtimepy/channel/environment/__init__.py,sha256=M8yEM-hrG7T0ls80G0yW9-2FkjQ9BRkPM2K42OPLfZI,1775
14
14
  runtimepy/channel/environment/array.py,sha256=9huqMYDj20vwJwSaFM4BVLj5D_JDuPemCTGde9gwN3M,3413
15
- runtimepy/channel/environment/base.py,sha256=fuPcfG5b8bR6k4GmJYjOGlWBmNWvyGLiYWn3lyMrsYw,14041
15
+ runtimepy/channel/environment/base.py,sha256=sR33vaIvHSmwXq0RBkThL_nU5qG87bn_6iKxEHCbRfY,14346
16
16
  runtimepy/channel/environment/create.py,sha256=DHjoNmzZsARjfB_CfutXQ1PDdxPETi6yQoRMhM0FbwA,5319
17
17
  runtimepy/channel/environment/file.py,sha256=PV05KZ3-CvftbKUM8acQmawOMeGGCcMrEESEBuymykg,6949
18
18
  runtimepy/channel/environment/sample.py,sha256=NnZ3g3OA5uoTJe7RSiR9FI_E-FFORPeZ6p1AYT30W9E,3880
@@ -29,11 +29,12 @@ runtimepy/codec/protocol/base.py,sha256=-BzSteAAG1XmKFztgUwOdMACztQ1YGSzwe_AM22V
29
29
  runtimepy/codec/protocol/json.py,sha256=pnj3UFmlXDjXzEa-l9jD6UhxsDA0aTnXhh8uIdBKhYg,4107
30
30
  runtimepy/codec/system/__init__.py,sha256=M9rIw0RvVGwz4JQhxbGWynUUE4H4L5HwJfXa5vI0CS8,6939
31
31
  runtimepy/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- runtimepy/commands/all.py,sha256=7lO655OdVspikWV-U91uxXuOT-dS_3E_j-K2Txt-eXk,1298
32
+ runtimepy/commands/all.py,sha256=BqD6SnXHzVwNcOLeToB4s3sF2cMQ6X1CpSYUsPl4-1Y,1456
33
33
  runtimepy/commands/arbiter.py,sha256=CtTMRYpqCAN3vWHkkr9jqWpoF7JGNXafKIBFmkarAfc,1567
34
34
  runtimepy/commands/common.py,sha256=NvZdeIFBHAF52c1n7vqD59DW6ywc-rG5iC5MpuhGf-c,2449
35
35
  runtimepy/commands/server.py,sha256=T5IwBeqwJPpg35Ms_Vmz6xS1T-8U3fcgiRU6mAFlkEU,3767
36
36
  runtimepy/commands/task.py,sha256=6xRVlRwpEZVhrcY18sQcfdWEOxeQZLeOF-6UrUURtO4,1435
37
+ runtimepy/commands/tftp.py,sha256=5dsrYSWbAY-ZAdvfukLOynkrJBtR4J1zEOZey4FerF4,2308
37
38
  runtimepy/commands/tui.py,sha256=9hWA3_YATibUUDTVQr7UnKzPTDVJ7WxWKTYYQpLoyrE,1869
38
39
  runtimepy/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
40
  runtimepy/control/source.py,sha256=nW3Q2D-LcekII7K5XKbxXCcR-9jYQyvv0UypeNy1Dnw,1695
@@ -46,7 +47,7 @@ runtimepy/data/favicon.ico,sha256=jgGIqM4gTrqPs3I6wajqKUH6Je885zy-jI7eU-Nl3AA,36
46
47
  runtimepy/data/server.yaml,sha256=wS_Ceiu2TpkfPurpqoYoPlgzc9DAWtUd24MW7t-S5rU,97
47
48
  runtimepy/data/server_base.yaml,sha256=QkF1xcHyMEViUhOc5rKFbFXjbRDDDHbX5LXvSQJRS58,615
48
49
  runtimepy/data/server_dev.yaml,sha256=nQsPh7LuQig3pzHfdg_aD3yOUiCj1sKKfI-WwW3hXmQ,523
49
- runtimepy/data/tftp_server.yaml,sha256=taAf7S2-2oAc-I4h62FRSihhiTIe0N-e2COl9apAYc0,204
50
+ runtimepy/data/tftp_server.yaml,sha256=-bFOWJSagI-fEQQcT8k7eDMJVfSPm2XAxLVG3dqUTa4,204
50
51
  runtimepy/data/css/bootstrap_extra.css,sha256=UyGXyv7nULrJ0UHsXTSM2IIrJ4X2XIEbPjmmU87yQno,1334
51
52
  runtimepy/data/css/main.css,sha256=h1iKkxg6-t7wTwNQvxDiD9mjpWrqq_FmgT6yXP7UANQ,688
52
53
  runtimepy/data/js/DataConnection.js,sha256=DnX8FMehjJXqmI62UMYXSvl_XdfQMzq3XUDFbLu2GgI,98
@@ -117,10 +118,10 @@ runtimepy/mixins/regex.py,sha256=kpCj4iL1akzt_KPPiMP-bTbuLBHOpkUrwbCTRe8HSUk,106
117
118
  runtimepy/mixins/trig.py,sha256=vkDd9Rh8080gvH5OHkSYmjImVgM_ZZ7RzMxsczKx5N4,2480
118
119
  runtimepy/net/__init__.py,sha256=leveti9T8P1WAg3M5N7yT_JrUZRCXwZQ8mRsxel-KP4,830
119
120
  runtimepy/net/backoff.py,sha256=IVloxQd-gEK62gFAlU2aOwgTEXhsNUTBfS16eiOqKG8,1080
120
- runtimepy/net/connection.py,sha256=wXnp3TXqpiRJTZYtuEUBGMV6_xteQVAXFYFw7h5Eh9Q,12891
121
+ runtimepy/net/connection.py,sha256=lg_cAGCAdOqlh3SURJRKDQ5TraiG7sV0kA_U2FGGNVU,12678
121
122
  runtimepy/net/manager.py,sha256=-M-ZSB9izay6HK1ytTayAYnSHYAz34dcwxaiNhC4lWg,4264
122
- runtimepy/net/mixin.py,sha256=u2BhWX0X1dLiQ-cU2F9Z2PgXdT9M8VB-QYWZ85asikk,2303
123
- runtimepy/net/util.py,sha256=lbTalWqBYLP8_OzC0t88fY6y-GUPabJtGWBB7cOBnQE,3545
123
+ runtimepy/net/mixin.py,sha256=0ySqjWlU-iqUS0Xgr2rzdbzZtbGoZEDLjF3TzHQpV8U,2271
124
+ runtimepy/net/util.py,sha256=kgOrNKzXdRPhsKU4GBmnJii6-Pcdnl5nDXI8IiSNYBQ,4017
124
125
  runtimepy/net/apps/__init__.py,sha256=vjo7e19QXtJwe6V6B-QGvYiJveYobnYIfpkKZrnS17w,710
125
126
  runtimepy/net/arbiter/__init__.py,sha256=ptKF995rYKvkm4Mya92vA5QEDqcFq5NRD0IYGqZ6_do,740
126
127
  runtimepy/net/arbiter/base.py,sha256=WRbgavarmOx6caQJmfI03udZvNC7o298uOhOsN-lp2E,14658
@@ -171,7 +172,7 @@ runtimepy/net/server/app/env/tab/__init__.py,sha256=stTVKyHljLQWnnhxkWPwa7bLdZtj
171
172
  runtimepy/net/server/app/env/tab/base.py,sha256=uBPpOeqI23341IezIQcGvJciPfIL2P5qQgbZ74aQRKA,991
172
173
  runtimepy/net/server/app/env/tab/controls.py,sha256=hsj0ErywfmOBtJLNZbMISrE2ELJEfyXvtCtpsDbXlYg,4734
173
174
  runtimepy/net/server/app/env/tab/html.py,sha256=pynwnRyD_jjME20psO7LK5mABqZudeWs3oDZCVHWRUs,7188
174
- runtimepy/net/server/app/env/tab/message.py,sha256=-7rIMvhJE_iWrn1gp4UqTKd-lYsjTMn5TsKtXjD1hFU,3798
175
+ runtimepy/net/server/app/env/tab/message.py,sha256=3LLaEIJgr2hhZH2-v-Sq-OKfKkfRgP-RDU8EpKLYb74,3709
175
176
  runtimepy/net/server/struct/__init__.py,sha256=z62I3DUoOnCcTv7C9X3sA6jOBKAJQ_uSOQA7uNwyrj4,1839
176
177
  runtimepy/net/server/websocket/__init__.py,sha256=KISuFUUQwNn6BXo8BOMuMOXyoVqE7Jw94ZQiSCQuRQE,5279
177
178
  runtimepy/net/server/websocket/state.py,sha256=qF6YtlQKY46gJ-jZlm8qZJb1lDfbXus5KmkZLqG9vJA,1711
@@ -182,31 +183,31 @@ runtimepy/net/stream/json/__init__.py,sha256=h--C_9moW92TC_e097FRRXcg8GJ6VVbMLXl
182
183
  runtimepy/net/tcp/__init__.py,sha256=OOWohegpoioSTf8M7uDf-4EV1IDungz7-U19L_2yW4I,250
183
184
  runtimepy/net/tcp/connection.py,sha256=8PbsdWQ32NTFAorEBVcJ3pPa9SpM0Y1FXm-FoSZ0JGs,7849
184
185
  runtimepy/net/tcp/create.py,sha256=yYlMuvV9N5i5PgwXrK8brpKdrQjmLNPtQsdEtnIMfH8,1940
185
- runtimepy/net/tcp/protocol.py,sha256=X5R0KE2KIwGMzHYnVtDeIATc-OGRoBuVebrYjnLOVBM,1510
186
+ runtimepy/net/tcp/protocol.py,sha256=vEnIX3gUX2nrw9ofT_e4KYU4VY2k4WP0WuOi4eE_OOQ,1444
186
187
  runtimepy/net/tcp/http/__init__.py,sha256=NeJi6Utmpc2m3D18DFMIlXfv3xymxX7OIzImTFTz4zI,5504
187
188
  runtimepy/net/tcp/scpi/__init__.py,sha256=aWCWQfdeyfoU9bpOnOtyIQbT1swl4ergXLFn5kXAH28,2105
188
189
  runtimepy/net/tcp/telnet/__init__.py,sha256=96eJFb301I3H2ivDtGMQtDDw09Xm5NRvM9VEC-wjt8c,4768
189
190
  runtimepy/net/tcp/telnet/codes.py,sha256=1-yyRe-Kz_W7d6B0P3iT1AaSNR3_Twmn-MUjKCJJknY,3518
190
191
  runtimepy/net/tcp/telnet/np_05b.py,sha256=ikp9txHBvbRps6x7C1FsEf9i6vmUtGvei4Nukc_gypI,7887
191
192
  runtimepy/net/udp/__init__.py,sha256=VSgle6cO2RXfwaei66wMIJ4zSB4gzpoadSSbmLfCzBw,357
192
- runtimepy/net/udp/connection.py,sha256=t49kWqSxjBnt5n1V-A5uBsYtWKor3B8YhI0E2ifKjOI,7763
193
+ runtimepy/net/udp/connection.py,sha256=YtLty6iveVH6PQRODKriwR5Q6lFi0Ed4k0o5r0bgA3A,7914
193
194
  runtimepy/net/udp/create.py,sha256=84YDfJbNBlN0ZwbNpvh6Dl7ZPecbZfmpjMNRRWcvJDk,2005
194
- runtimepy/net/udp/protocol.py,sha256=vAF3DUvNCWKhzKzoh-0rdH1mhjp-ar1ojSaQCuGcjq0,1590
195
+ runtimepy/net/udp/protocol.py,sha256=A4SRHf0CgcL2zDs1nAsGDqz0RxKBy1soS8wtNdS5S0I,1492
195
196
  runtimepy/net/udp/queue.py,sha256=DF-YscxQcGbGCYQLz_l_BMaSRfraZOhRwieTEdXLMds,637
196
- runtimepy/net/udp/tftp/__init__.py,sha256=KAxgPsyTBY8gy28YVLcMw2EHlXcMUw_IVqaqluj6Fz8,7767
197
- runtimepy/net/udp/tftp/base.py,sha256=Wii9wN6d_YKTF4lZMOaPbG6oa5YO8AYMHC-2EkkZgoI,8320
198
- runtimepy/net/udp/tftp/endpoint.py,sha256=OaIto6nzF_lQ5oJGU64jPW5WJ17ealzEUHA3nDndZG4,10556
197
+ runtimepy/net/udp/tftp/__init__.py,sha256=SJGkizBqV73XfQ9HIRAcFE8Xy9JXJHptr_WtbFswZgc,8724
198
+ runtimepy/net/udp/tftp/base.py,sha256=FCrtpLdUFkQU4nkn5vJc6Gn5EsZauCf8eLhxx1r_vG8,11276
199
+ runtimepy/net/udp/tftp/endpoint.py,sha256=so60LdPTG66N5tdhHhiX7j_TBHvNOTi4JIgLcg2MAm0,10890
199
200
  runtimepy/net/udp/tftp/enums.py,sha256=06juMd__pJZsyL8zO8p3hRucnOratt1qtz9zcxzMg4s,1579
200
201
  runtimepy/net/udp/tftp/io.py,sha256=w6cnUt-T-Ma6Vg8BWoRbsNnIWUv0HTY4am6bcLWxNJs,803
201
202
  runtimepy/net/websocket/__init__.py,sha256=YjSmoxiigmsI_hcQw6nueX7bxhrRGerEERnPvgLVEVA,313
202
203
  runtimepy/net/websocket/connection.py,sha256=Q4TDXcEXrUv1IfNg1veMxag9AyvvYmuYsnzufYaWnD8,8274
203
204
  runtimepy/noise/__init__.py,sha256=EJM7h3t_z74wwrn6FAFQwYE2yUcOZQ1K1IQqOb8Z0AI,384
204
205
  runtimepy/primitives/__init__.py,sha256=nwWJH1e0KN2NsVwQ3wvRtUpl9s9Ap8Q32NNZLGol0wU,2323
205
- runtimepy/primitives/base.py,sha256=MZ5kpWKh9cgwK5JU5773ic4z13Ri-lJH-75RJ3HyaNE,9078
206
+ runtimepy/primitives/base.py,sha256=BaGPUTeVMnLnTPcpjqnS2lzPN74Pe5C0XaQdgrTfW7A,9185
206
207
  runtimepy/primitives/bool.py,sha256=c-IRpVZ84m-pOreCHC382tOW0NFKEwSTiEeXAtlJjvk,1243
207
208
  runtimepy/primitives/byte_order.py,sha256=80mMk1Sj_l49XvAtvrPmoYFpFYSM1HgYuwR2-P7os3Q,767
208
209
  runtimepy/primitives/evaluation.py,sha256=0N7mT8uoiJaY-coF2PeEXU2WO-FmbyN2Io9_EaghO9Q,4657
209
- runtimepy/primitives/float.py,sha256=oQg-diQkiIFLeT5e-2bMVSva6dw_SYUSmD5k9vErdeI,1667
210
+ runtimepy/primitives/float.py,sha256=aeEsj0xRJM57Hcv04OtLfT_sTBocZldbn19HpQq3Hxs,1946
210
211
  runtimepy/primitives/int.py,sha256=Zch7iIW9xsLmAtmIYWY9lWOJpv6dbZT102J2Yey66gc,3018
211
212
  runtimepy/primitives/scaling.py,sha256=Vtxp2CSBahqPp4i2-IS4wjbcC023xwf-dqZMbYWf3V4,1144
212
213
  runtimepy/primitives/string.py,sha256=ic5VKhXCSIwEOUfqIb1VUpZPwjdAcBul-cLLIihVkQI,2532
@@ -216,7 +217,7 @@ runtimepy/primitives/field/fields.py,sha256=jDNi1tl2Xc3GBmt6QJuqxbhP8MtxgertGbPF
216
217
  runtimepy/primitives/field/manager/__init__.py,sha256=BCRi6-_5OOJ8kz78JHkiLp8cZ71KA1uiF2zq5FFe9js,2586
217
218
  runtimepy/primitives/field/manager/base.py,sha256=EyWs5D9_reKOTLkh8PuW45ySjCh31fY_qrtFIcmIOV4,6914
218
219
  runtimepy/primitives/serializable/__init__.py,sha256=mZ8KZe2aQswGL6GTjQ8-IaUQwg-6aUUBP5RXyzR6crc,393
219
- runtimepy/primitives/serializable/base.py,sha256=ci-C5jCCaqtkUu-ofGEJlsJp_zF8taTHomEnvY18k08,4413
220
+ runtimepy/primitives/serializable/base.py,sha256=piviwBXWV6dvbppaVnH-k1fmgVbE8rHvHvZogb2jsLw,4496
220
221
  runtimepy/primitives/serializable/fixed.py,sha256=rhr6uVbo0Lvazk4fLI7iei-vVNEwP1J8-LoUjW1NaMI,1077
221
222
  runtimepy/primitives/serializable/prefixed.py,sha256=oQXW0pGRovKolheL5ZL2m9aNVMCtKTAi5OlC9KW0iKI,2855
222
223
  runtimepy/primitives/types/__init__.py,sha256=JUJpDFIjDUYo-Jnx5sZnkmbGLVjIHVsRcvBjVlJ7fsA,1588
@@ -254,9 +255,9 @@ runtimepy/tui/task.py,sha256=nUZo9fuOC-k1Wpqdzkv9v1tQirCI28fZVgcC13Ijvus,1093
254
255
  runtimepy/tui/channels/__init__.py,sha256=evDaiIn-YS9uGhdo8ZGtP9VK1ek6sr_P1nJ9JuSET0o,4536
255
256
  runtimepy/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
256
257
  runtimepy/ui/controls.py,sha256=yvT7h3thbYaitsakcIAJ90EwKzJ4b-jnc6p3UuVf_XE,1241
257
- runtimepy-5.4.0.dist-info/LICENSE,sha256=okYCYhGsx_BlzvFdoNVBVpw_Cfb4SOqHA_VAARml4Hc,1071
258
- runtimepy-5.4.0.dist-info/METADATA,sha256=WeXzlPriB6OFKdK97iJY_zCvq9gTg_yM-LxRLyQDHmc,7371
259
- runtimepy-5.4.0.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
260
- runtimepy-5.4.0.dist-info/entry_points.txt,sha256=-btVBkYv7ybcopqZ_pRky-bEzu3vhbaG3W3Z7ERBiFE,51
261
- runtimepy-5.4.0.dist-info/top_level.txt,sha256=0jPmh6yqHyyJJDwEID-LpQly-9kQ3WRMjH7Lix8peLg,10
262
- runtimepy-5.4.0.dist-info/RECORD,,
258
+ runtimepy-5.4.2.dist-info/LICENSE,sha256=okYCYhGsx_BlzvFdoNVBVpw_Cfb4SOqHA_VAARml4Hc,1071
259
+ runtimepy-5.4.2.dist-info/METADATA,sha256=HuPnRnjp8IDWbQUxSwohDtt4GvNFQJYvZ8rgY_NfyoQ,8152
260
+ runtimepy-5.4.2.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
261
+ runtimepy-5.4.2.dist-info/entry_points.txt,sha256=-btVBkYv7ybcopqZ_pRky-bEzu3vhbaG3W3Z7ERBiFE,51
262
+ runtimepy-5.4.2.dist-info/top_level.txt,sha256=0jPmh6yqHyyJJDwEID-LpQly-9kQ3WRMjH7Lix8peLg,10
263
+ runtimepy-5.4.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.3.0)
2
+ Generator: setuptools (71.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5