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 +2 -2
- runtimepy/channel/environment/base.py +12 -4
- runtimepy/commands/all.py +7 -1
- runtimepy/commands/tftp.py +91 -0
- runtimepy/data/tftp_server.yaml +1 -1
- runtimepy/entry.py +5 -3
- runtimepy/net/connection.py +1 -8
- runtimepy/net/mixin.py +0 -1
- runtimepy/net/server/app/env/tab/message.py +4 -7
- runtimepy/net/tcp/protocol.py +0 -2
- runtimepy/net/udp/connection.py +8 -5
- runtimepy/net/udp/protocol.py +0 -3
- runtimepy/net/udp/tftp/__init__.py +57 -32
- runtimepy/net/udp/tftp/base.py +85 -8
- runtimepy/net/udp/tftp/endpoint.py +18 -8
- runtimepy/net/util.py +18 -0
- runtimepy/primitives/base.py +15 -12
- runtimepy/primitives/float.py +10 -0
- runtimepy/primitives/serializable/base.py +5 -1
- {runtimepy-5.4.0.dist-info → runtimepy-5.4.2.dist-info}/METADATA +32 -6
- {runtimepy-5.4.0.dist-info → runtimepy-5.4.2.dist-info}/RECORD +25 -24
- {runtimepy-5.4.0.dist-info → runtimepy-5.4.2.dist-info}/WHEEL +1 -1
- {runtimepy-5.4.0.dist-info → runtimepy-5.4.2.dist-info}/LICENSE +0 -0
- {runtimepy-5.4.0.dist-info → runtimepy-5.4.2.dist-info}/entry_points.txt +0 -0
- {runtimepy-5.4.0.dist-info → runtimepy-5.4.2.dist-info}/top_level.txt +0 -0
runtimepy/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# =====================================
|
|
2
2
|
# generator=datazen
|
|
3
3
|
# version=3.1.4
|
|
4
|
-
# hash=
|
|
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.
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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=
|
|
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
|
runtimepy/data/tftp_server.yaml
CHANGED
runtimepy/entry.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# =====================================
|
|
2
2
|
# generator=datazen
|
|
3
3
|
# version=3.1.4
|
|
4
|
-
# hash=
|
|
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
|
-
|
|
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):
|
runtimepy/net/connection.py
CHANGED
|
@@ -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
|
@@ -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
|
|
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
|
|
runtimepy/net/tcp/protocol.py
CHANGED
|
@@ -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."""
|
runtimepy/net/udp/connection.py
CHANGED
|
@@ -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
|
|
runtimepy/net/udp/protocol.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
endpoint.
|
|
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,
|
|
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
|
)
|
runtimepy/net/udp/tftp/base.py
CHANGED
|
@@ -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.
|
|
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[
|
|
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
|
-
|
|
172
|
+
addr = normalize_host(*addr)
|
|
121
173
|
|
|
122
|
-
if
|
|
123
|
-
self._endpoints[
|
|
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[
|
|
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
|
-
|
|
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)
|
|
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:
|
|
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
|
|
72
|
-
self.timeout
|
|
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
|
|
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]
|
runtimepy/primitives/base.py
CHANGED
|
@@ -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:
|
runtimepy/primitives/float.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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=
|
|
47
|
+
hash=4f8a71a6066638ed1a90f375188f0578
|
|
48
48
|
=====================================
|
|
49
49
|
-->
|
|
50
50
|
|
|
51
|
-
# runtimepy ([5.4.
|
|
51
|
+
# runtimepy ([5.4.2](https://pypi.org/project/runtimepy/))
|
|
52
52
|
|
|
53
53
|
[](https://pypi.org/project/runtimepy/)
|
|
54
54
|

|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
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=
|
|
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=
|
|
123
|
-
runtimepy/net/util.py,sha256=
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
197
|
-
runtimepy/net/udp/tftp/base.py,sha256=
|
|
198
|
-
runtimepy/net/udp/tftp/endpoint.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
258
|
-
runtimepy-5.4.
|
|
259
|
-
runtimepy-5.4.
|
|
260
|
-
runtimepy-5.4.
|
|
261
|
-
runtimepy-5.4.
|
|
262
|
-
runtimepy-5.4.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|