runtimepy 5.14.2__py3-none-any.whl → 5.15.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runtimepy/__init__.py +2 -2
- runtimepy/channel/__init__.py +1 -4
- runtimepy/channel/environment/__init__.py +93 -2
- runtimepy/channel/environment/create.py +16 -1
- runtimepy/channel/environment/sample.py +10 -2
- runtimepy/channel/registry.py +2 -3
- runtimepy/codec/protocol/base.py +34 -14
- runtimepy/codec/protocol/json.py +5 -3
- runtimepy/codec/system/__init__.py +6 -2
- runtimepy/control/source.py +1 -1
- runtimepy/data/404.md +16 -0
- runtimepy/data/base.yaml +3 -0
- runtimepy/data/css/bootstrap_extra.css +59 -44
- runtimepy/data/css/main.css +23 -4
- runtimepy/data/dummy_load.yaml +5 -2
- runtimepy/data/factories.yaml +1 -0
- runtimepy/data/js/classes/App.js +54 -2
- runtimepy/data/js/classes/ChannelTable.js +6 -8
- runtimepy/data/js/classes/Plot.js +9 -4
- runtimepy/data/js/classes/TabFilter.js +47 -9
- runtimepy/data/js/classes/TabInterface.js +106 -11
- runtimepy/data/js/classes/WindowHashManager.js +30 -15
- runtimepy/data/js/init.js +18 -1
- runtimepy/data/js/markdown_page.js +10 -0
- runtimepy/data/js/sample.js +1 -0
- runtimepy/data/schemas/BitFields.yaml +9 -0
- runtimepy/data/schemas/RuntimeEnum.yaml +6 -0
- runtimepy/data/schemas/StructConfig.yaml +9 -1
- runtimepy/data/static/css/bootstrap-icons.min.css +4 -3
- runtimepy/data/static/css/bootstrap.min.css +3 -4
- runtimepy/data/static/css/fonts/bootstrap-icons.woff +0 -0
- runtimepy/data/static/css/fonts/bootstrap-icons.woff2 +0 -0
- runtimepy/data/static/js/bootstrap.bundle.min.js +5 -4
- runtimepy/data/static/js/webglplot.umd.min.js +2 -1
- runtimepy/data/static/svg/outline-dark.svg +22 -0
- runtimepy/data/static/svg/outline-light.svg +22 -0
- runtimepy/enum/__init__.py +13 -1
- runtimepy/enum/registry.py +13 -1
- runtimepy/message/__init__.py +3 -3
- runtimepy/mixins/logging.py +6 -1
- runtimepy/net/__init__.py +0 -2
- runtimepy/net/arbiter/info.py +36 -4
- runtimepy/net/arbiter/struct/__init__.py +3 -2
- runtimepy/net/connection.py +6 -7
- runtimepy/net/html/__init__.py +29 -11
- runtimepy/net/html/bootstrap/__init__.py +2 -2
- runtimepy/net/html/bootstrap/elements.py +44 -24
- runtimepy/net/html/bootstrap/tabs.py +18 -11
- runtimepy/net/http/__init__.py +3 -3
- runtimepy/net/http/request_target.py +3 -3
- runtimepy/net/mixin.py +4 -2
- runtimepy/net/server/__init__.py +16 -9
- runtimepy/net/server/app/__init__.py +1 -0
- runtimepy/net/server/app/create.py +3 -3
- runtimepy/net/server/app/env/__init__.py +30 -4
- runtimepy/net/server/app/env/settings.py +4 -7
- runtimepy/net/server/app/env/tab/base.py +2 -1
- runtimepy/net/server/app/env/tab/controls.py +141 -27
- runtimepy/net/server/app/env/tab/html.py +68 -26
- runtimepy/net/server/app/env/widgets.py +115 -61
- runtimepy/net/server/app/landing_page.py +1 -1
- runtimepy/net/server/app/tab.py +12 -3
- runtimepy/net/server/html.py +2 -2
- runtimepy/net/server/json.py +1 -1
- runtimepy/net/server/markdown.py +29 -12
- runtimepy/net/server/mux.py +29 -0
- runtimepy/net/stream/__init__.py +6 -5
- runtimepy/net/stream/base.py +4 -2
- runtimepy/net/tcp/connection.py +5 -3
- runtimepy/net/tcp/http/__init__.py +10 -9
- runtimepy/net/tcp/protocol.py +2 -2
- runtimepy/net/tcp/scpi/__init__.py +5 -2
- runtimepy/net/tcp/telnet/__init__.py +2 -1
- runtimepy/net/udp/connection.py +10 -6
- runtimepy/net/udp/protocol.py +5 -6
- runtimepy/net/udp/queue.py +5 -2
- runtimepy/net/udp/tftp/base.py +2 -1
- runtimepy/net/websocket/connection.py +58 -9
- runtimepy/primitives/array/__init__.py +7 -5
- runtimepy/primitives/base.py +3 -2
- runtimepy/primitives/field/__init__.py +35 -2
- runtimepy/primitives/field/fields.py +11 -2
- runtimepy/primitives/field/manager/base.py +19 -2
- runtimepy/primitives/serializable/base.py +5 -2
- runtimepy/primitives/serializable/fixed.py +5 -2
- runtimepy/primitives/serializable/prefixed.py +4 -1
- runtimepy/primitives/types/base.py +4 -1
- runtimepy/primitives/types/bounds.py +10 -4
- runtimepy/registry/__init__.py +20 -0
- runtimepy/registry/name.py +6 -0
- runtimepy/requirements.txt +2 -2
- runtimepy/ui/controls.py +20 -1
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/METADATA +6 -6
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/RECORD +98 -94
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/WHEEL +1 -1
- runtimepy/data/404.html +0 -7
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/entry_points.txt +0 -0
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/licenses/LICENSE +0 -0
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/top_level.txt +0 -0
runtimepy/enum/registry.py
CHANGED
|
@@ -6,6 +6,7 @@ A module implementing and enumeration registry.
|
|
|
6
6
|
from enum import IntEnum as _IntEnum
|
|
7
7
|
from typing import Optional as _Optional
|
|
8
8
|
from typing import TypeVar
|
|
9
|
+
from typing import Union as _Union
|
|
9
10
|
from typing import cast as _cast
|
|
10
11
|
|
|
11
12
|
# third-party
|
|
@@ -34,12 +35,15 @@ class EnumRegistry(_Registry[_RuntimeEnum]):
|
|
|
34
35
|
kind: _EnumTypelike,
|
|
35
36
|
items: _EnumMappingData = None,
|
|
36
37
|
primitive: str = DEFAULT_ENUM_PRIMITIVE,
|
|
38
|
+
default: _Union[str, bool, int] = None,
|
|
37
39
|
) -> _Optional[_RuntimeEnum]:
|
|
38
40
|
"""Create a new runtime enumeration."""
|
|
39
41
|
|
|
40
42
|
data: _JsonObject = {"type": _cast(str, kind), "primitive": primitive}
|
|
41
43
|
if items is not None:
|
|
42
44
|
data["items"] = items # type: ignore
|
|
45
|
+
if default is not None:
|
|
46
|
+
data["default"] = default
|
|
43
47
|
return self.register_dict(name, data)
|
|
44
48
|
|
|
45
49
|
|
|
@@ -68,6 +72,11 @@ class RuntimeIntEnum(_IntEnum):
|
|
|
68
72
|
"""Override in sub-class to coerce enum id."""
|
|
69
73
|
return None
|
|
70
74
|
|
|
75
|
+
@classmethod
|
|
76
|
+
def default(cls: type[T]) -> _Optional[T]:
|
|
77
|
+
"""Get a possible default value for this enumeration."""
|
|
78
|
+
return None
|
|
79
|
+
|
|
71
80
|
@classmethod
|
|
72
81
|
def primitive(cls) -> str:
|
|
73
82
|
"""The underlying primitive type for this runtime enumeration."""
|
|
@@ -81,7 +90,7 @@ class RuntimeIntEnum(_IntEnum):
|
|
|
81
90
|
@classmethod
|
|
82
91
|
def runtime_enum(cls, identifier: int) -> _RuntimeEnum:
|
|
83
92
|
"""Obtain a runtime enumeration from this class."""
|
|
84
|
-
return _RuntimeEnum.from_enum(cls, identifier)
|
|
93
|
+
return _RuntimeEnum.from_enum(cls, identifier, default=cls.default())
|
|
85
94
|
|
|
86
95
|
@classmethod
|
|
87
96
|
def register_enum(
|
|
@@ -99,6 +108,9 @@ class RuntimeIntEnum(_IntEnum):
|
|
|
99
108
|
if ident is not None:
|
|
100
109
|
data["id"] = ident
|
|
101
110
|
|
|
111
|
+
if cls.default() is not None:
|
|
112
|
+
data["default"] = cls.default()
|
|
113
|
+
|
|
102
114
|
result = registry.register_dict(name, data)
|
|
103
115
|
assert result is not None, (name, data)
|
|
104
116
|
return result
|
runtimepy/message/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ from typing import Any
|
|
|
9
9
|
from typing import Iterator as _Iterator
|
|
10
10
|
|
|
11
11
|
# third-party
|
|
12
|
-
from vcorelib.io import ByteFifo
|
|
12
|
+
from vcorelib.io import BinaryMessage, ByteFifo
|
|
13
13
|
|
|
14
14
|
# internal
|
|
15
15
|
from runtimepy.primitives import Uint32, UnsignedInt
|
|
@@ -48,7 +48,7 @@ class MessageProcessor:
|
|
|
48
48
|
|
|
49
49
|
self.message_length_out = self.message_length_kind()
|
|
50
50
|
|
|
51
|
-
def encode(self, stream: _BytesIO, data:
|
|
51
|
+
def encode(self, stream: _BytesIO, data: BinaryMessage | str) -> None:
|
|
52
52
|
"""Encode a message to a stream."""
|
|
53
53
|
|
|
54
54
|
if isinstance(data, str):
|
|
@@ -72,7 +72,7 @@ class MessageProcessor:
|
|
|
72
72
|
for message in self.process(data):
|
|
73
73
|
yield loads(message.decode())
|
|
74
74
|
|
|
75
|
-
def process(self, data:
|
|
75
|
+
def process(self, data: BinaryMessage) -> _Iterator[bytearray]:
|
|
76
76
|
"""Process an incoming message."""
|
|
77
77
|
|
|
78
78
|
self.buffer.ingest(data)
|
runtimepy/mixins/logging.py
CHANGED
|
@@ -6,7 +6,7 @@ A module implementing a logger-mixin extension.
|
|
|
6
6
|
from contextlib import AsyncExitStack
|
|
7
7
|
import io
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Any, Iterable
|
|
9
|
+
from typing import Any, Iterable, Optional
|
|
10
10
|
|
|
11
11
|
# third-party
|
|
12
12
|
import aiofiles
|
|
@@ -27,6 +27,11 @@ class LogLevel(RuntimeIntEnum):
|
|
|
27
27
|
ERROR = logging.ERROR
|
|
28
28
|
CRITICAL = logging.CRITICAL
|
|
29
29
|
|
|
30
|
+
@classmethod
|
|
31
|
+
def id(cls) -> Optional[int]:
|
|
32
|
+
"""Override in sub-class to coerce enum id."""
|
|
33
|
+
return 2
|
|
34
|
+
|
|
30
35
|
|
|
31
36
|
LogLevellike = LogLevel | int | str
|
|
32
37
|
|
runtimepy/net/__init__.py
CHANGED
|
@@ -5,7 +5,6 @@ A module aggregating commonly used networking interface.
|
|
|
5
5
|
# internal
|
|
6
6
|
from runtimepy.net.backoff import ExponentialBackoff
|
|
7
7
|
from runtimepy.net.connection import (
|
|
8
|
-
BinaryMessage,
|
|
9
8
|
Connection,
|
|
10
9
|
EchoConnection,
|
|
11
10
|
NullConnection,
|
|
@@ -24,7 +23,6 @@ from runtimepy.net.util import (
|
|
|
24
23
|
)
|
|
25
24
|
|
|
26
25
|
__all__ = [
|
|
27
|
-
"BinaryMessage",
|
|
28
26
|
"Connection",
|
|
29
27
|
"EchoConnection",
|
|
30
28
|
"NullConnection",
|
runtimepy/net/arbiter/info.py
CHANGED
|
@@ -3,29 +3,33 @@ A module implementing an application information interface.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
# built-in
|
|
6
|
-
from abc import ABC as _ABC
|
|
7
6
|
import asyncio as _asyncio
|
|
8
7
|
from contextlib import AsyncExitStack as _AsyncExitStack
|
|
9
8
|
from contextlib import contextmanager
|
|
10
9
|
from dataclasses import dataclass
|
|
10
|
+
from importlib import import_module
|
|
11
11
|
from logging import getLogger as _getLogger
|
|
12
12
|
from re import compile as _compile
|
|
13
13
|
from typing import Awaitable as _Awaitable
|
|
14
14
|
from typing import Callable as _Callable
|
|
15
15
|
from typing import Iterator as _Iterator
|
|
16
16
|
from typing import MutableMapping as _MutableMapping
|
|
17
|
+
from typing import Optional
|
|
17
18
|
from typing import TYPE_CHECKING, Any
|
|
18
19
|
from typing import TypeVar as _TypeVar
|
|
19
20
|
from typing import Union as _Union
|
|
21
|
+
from typing import cast
|
|
20
22
|
|
|
21
23
|
# third-party
|
|
22
24
|
from vcorelib.io.types import JsonObject as _JsonObject
|
|
23
25
|
from vcorelib.logging import LoggerMixin as _LoggerMixin
|
|
24
26
|
from vcorelib.logging import LoggerType as _LoggerType
|
|
27
|
+
from vcorelib.names import import_str_and_item
|
|
25
28
|
from vcorelib.namespace import Namespace as _Namespace
|
|
26
29
|
|
|
27
30
|
# internal
|
|
28
31
|
from runtimepy.channel.environment.sample import poll_sample_env, sample_env
|
|
32
|
+
from runtimepy.codec.protocol import Protocol, ProtocolFactory
|
|
29
33
|
from runtimepy.mapping import DEFAULT_PATTERN
|
|
30
34
|
from runtimepy.mixins.trig import TrigMixin
|
|
31
35
|
from runtimepy.net.arbiter.result import OverallResult, results
|
|
@@ -50,11 +54,13 @@ V = _TypeVar("V", bound=PeriodicTask)
|
|
|
50
54
|
Z = _TypeVar("Z")
|
|
51
55
|
|
|
52
56
|
|
|
53
|
-
class RuntimeStruct(RuntimeStructBase
|
|
57
|
+
class RuntimeStruct(RuntimeStructBase):
|
|
54
58
|
"""A class implementing a base runtime structure."""
|
|
55
59
|
|
|
56
60
|
app: "AppInfo"
|
|
57
61
|
array: PrimitiveArray
|
|
62
|
+
recv: Optional[Protocol]
|
|
63
|
+
send: Optional[Protocol]
|
|
58
64
|
|
|
59
65
|
byte_order: ByteOrder = DEFAULT_BYTE_ORDER
|
|
60
66
|
|
|
@@ -81,13 +87,39 @@ class RuntimeStruct(RuntimeStructBase, _ABC):
|
|
|
81
87
|
if self.final_poll:
|
|
82
88
|
self.app.stack.enter_context(self._final_poll())
|
|
83
89
|
|
|
90
|
+
byte_order = type(self).byte_order
|
|
91
|
+
|
|
92
|
+
self.recv = None
|
|
93
|
+
self.send = None
|
|
94
|
+
if "protocol_factory" in self.config:
|
|
95
|
+
module, name = import_str_and_item(
|
|
96
|
+
cast(str, self.config["protocol_factory"])
|
|
97
|
+
)
|
|
98
|
+
factory: type[ProtocolFactory] = getattr(
|
|
99
|
+
import_module(module), name
|
|
100
|
+
)
|
|
101
|
+
if ProtocolFactory in factory.__bases__:
|
|
102
|
+
self.recv = factory.singleton()
|
|
103
|
+
byte_order = self.recv.byte_order
|
|
104
|
+
with self.env.names_pushed("rx"):
|
|
105
|
+
self.env.register_protocol(self.recv, False)
|
|
106
|
+
|
|
107
|
+
self.send = factory.instance()
|
|
108
|
+
with self.env.names_pushed("tx"):
|
|
109
|
+
self.env.register_protocol(self.send, True)
|
|
110
|
+
|
|
84
111
|
self.init_env()
|
|
85
|
-
self.update_byte_order(
|
|
112
|
+
self.update_byte_order(byte_order, **kwargs)
|
|
86
113
|
|
|
87
114
|
def update_byte_order(self, byte_order: ByteOrder, **kwargs) -> None:
|
|
88
115
|
"""Update the over-the-wire byte order for this struct."""
|
|
89
116
|
|
|
90
|
-
self.
|
|
117
|
+
if self.recv is None:
|
|
118
|
+
self.array = self.env.array(byte_order=byte_order, **kwargs).array
|
|
119
|
+
else:
|
|
120
|
+
# Can't change byte order at runtime in this configuration.
|
|
121
|
+
assert byte_order == self.recv.byte_order
|
|
122
|
+
self.array = self.recv
|
|
91
123
|
|
|
92
124
|
|
|
93
125
|
W = _TypeVar("W", bound=RuntimeStruct)
|
|
@@ -7,6 +7,7 @@ from io import BytesIO
|
|
|
7
7
|
from typing import Generic, Iterator, Optional, TypeVar
|
|
8
8
|
|
|
9
9
|
# third-party
|
|
10
|
+
from vcorelib.io import BinaryMessage
|
|
10
11
|
from vcorelib.math import default_time_ns
|
|
11
12
|
from vcorelib.math.keeper import TimeSource
|
|
12
13
|
|
|
@@ -67,7 +68,7 @@ class TimestampedStruct(RuntimeStruct):
|
|
|
67
68
|
self.array.update(data, timestamp_ns=timestamp_ns)
|
|
68
69
|
return timestamp_ns
|
|
69
70
|
|
|
70
|
-
def process_datagram(self, data:
|
|
71
|
+
def process_datagram(self, data: BinaryMessage) -> Iterator[int]:
|
|
71
72
|
"""Process an array message."""
|
|
72
73
|
|
|
73
74
|
size = self.array.size
|
|
@@ -190,7 +191,7 @@ class UdpStructTransceiver(UdpConnection, Generic[T]):
|
|
|
190
191
|
"""Handle individual struct updates."""
|
|
191
192
|
|
|
192
193
|
async def process_datagram(
|
|
193
|
-
self, data:
|
|
194
|
+
self, data: BinaryMessage, addr: tuple[str, int]
|
|
194
195
|
) -> bool:
|
|
195
196
|
"""Process an array of struct instances."""
|
|
196
197
|
|
runtimepy/net/connection.py
CHANGED
|
@@ -13,6 +13,7 @@ from typing import Union as _Union
|
|
|
13
13
|
|
|
14
14
|
# third-party
|
|
15
15
|
from vcorelib.asyncio import log_exceptions as _log_exceptions
|
|
16
|
+
from vcorelib.io import BinaryMessage
|
|
16
17
|
from vcorelib.io.markdown import MarkdownMixin
|
|
17
18
|
from vcorelib.logging import LoggerType as _LoggerType
|
|
18
19
|
|
|
@@ -29,8 +30,6 @@ from runtimepy.net.backoff import ExponentialBackoff
|
|
|
29
30
|
from runtimepy.primitives import Bool, Uint8
|
|
30
31
|
from runtimepy.primitives.byte_order import DEFAULT_BYTE_ORDER, ByteOrder
|
|
31
32
|
|
|
32
|
-
BinaryMessage = _Union[bytes, bytearray, memoryview]
|
|
33
|
-
|
|
34
33
|
|
|
35
34
|
class Connection(
|
|
36
35
|
LoggerMixinLevelControl, ChannelEnvironmentMixin, MarkdownMixin, _ABC
|
|
@@ -142,7 +141,7 @@ class Connection(
|
|
|
142
141
|
"""Process a text frame."""
|
|
143
142
|
raise NotImplementedError
|
|
144
143
|
|
|
145
|
-
async def process_binary(self, data:
|
|
144
|
+
async def process_binary(self, data: BinaryMessage) -> bool:
|
|
146
145
|
"""Process a binary frame."""
|
|
147
146
|
raise NotImplementedError
|
|
148
147
|
|
|
@@ -251,9 +250,9 @@ class Connection(
|
|
|
251
250
|
await backoff.sleep()
|
|
252
251
|
if await self.restart():
|
|
253
252
|
self._set_enabled(True)
|
|
254
|
-
self._restarts.
|
|
253
|
+
self._restarts.increment()
|
|
255
254
|
|
|
256
|
-
self._restart_attempts.
|
|
255
|
+
self._restart_attempts.increment()
|
|
257
256
|
|
|
258
257
|
@asynccontextmanager
|
|
259
258
|
async def process_then_disable(self, **kwargs) -> AsyncIterator[None]:
|
|
@@ -391,7 +390,7 @@ class EchoConnection(Connection):
|
|
|
391
390
|
self.send_text(data)
|
|
392
391
|
return True
|
|
393
392
|
|
|
394
|
-
async def process_binary(self, data:
|
|
393
|
+
async def process_binary(self, data: BinaryMessage) -> bool:
|
|
395
394
|
"""Process a binary frame."""
|
|
396
395
|
self.send_binary(data)
|
|
397
396
|
return True
|
|
@@ -404,6 +403,6 @@ class NullConnection(Connection):
|
|
|
404
403
|
"""Process a text frame."""
|
|
405
404
|
return True
|
|
406
405
|
|
|
407
|
-
async def process_binary(self, data:
|
|
406
|
+
async def process_binary(self, data: BinaryMessage) -> bool:
|
|
408
407
|
"""Process a binary frame."""
|
|
409
408
|
return True
|
runtimepy/net/html/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ A module implementing HTML-related interfaces.
|
|
|
5
5
|
# built-in
|
|
6
6
|
from io import StringIO
|
|
7
7
|
from typing import Any, Optional
|
|
8
|
-
from urllib.parse import parse_qs
|
|
8
|
+
from urllib.parse import parse_qs, urlencode
|
|
9
9
|
|
|
10
10
|
# third-party
|
|
11
11
|
from svgen.element import Element
|
|
@@ -13,7 +13,6 @@ from svgen.element.html import Html, div
|
|
|
13
13
|
from vcorelib import DEFAULT_ENCODING
|
|
14
14
|
from vcorelib.io import IndentedFileWriter
|
|
15
15
|
from vcorelib.paths import find_file
|
|
16
|
-
from vcorelib.python import StrToBool
|
|
17
16
|
|
|
18
17
|
# internal
|
|
19
18
|
from runtimepy import PKG_NAME
|
|
@@ -37,20 +36,30 @@ def create_app_shell(
|
|
|
37
36
|
"""Create a bootstrap-based application shell."""
|
|
38
37
|
|
|
39
38
|
container = div(parent=parent, **kwargs)
|
|
40
|
-
container.add_class("d-flex", "align-items-start", "bg-body")
|
|
39
|
+
container.add_class("d-flex", "align-items-start", "bg-body", "h-100")
|
|
41
40
|
|
|
42
41
|
# Dark theme.
|
|
43
42
|
container["data-bs-theme"] = bootstrap_theme
|
|
44
43
|
|
|
45
44
|
# Buttons.
|
|
46
|
-
button_column = div(
|
|
45
|
+
button_column = div(
|
|
46
|
+
id="button-column",
|
|
47
|
+
parent=container if use_button_column else None,
|
|
48
|
+
head_child=div(
|
|
49
|
+
class_str="flex-grow-1 border-bottom "
|
|
50
|
+
"bg-gradient-secondary-to-bottom"
|
|
51
|
+
),
|
|
52
|
+
tail_child=div(
|
|
53
|
+
class_str="flex-grow-1 border-top bg-gradient-secondary-to-top"
|
|
54
|
+
),
|
|
55
|
+
)
|
|
47
56
|
button_column.add_class(
|
|
48
57
|
"d-flex", "flex-column", "h-100", f"bg-{bootstrap_theme}-subtle"
|
|
49
58
|
)
|
|
50
59
|
|
|
51
60
|
# Dark/light theme switch button.
|
|
52
61
|
bootstrap_button(
|
|
53
|
-
icon_str("
|
|
62
|
+
icon_str("highlights"),
|
|
54
63
|
tooltip="Toggle light/dark.",
|
|
55
64
|
id="theme-button",
|
|
56
65
|
parent=button_column,
|
|
@@ -95,26 +104,35 @@ def full_markdown_page(
|
|
|
95
104
|
|
|
96
105
|
markdown_kwargs: dict[str, Any] = {"id": PKG_NAME}
|
|
97
106
|
|
|
107
|
+
params: dict[str, str] = {"print": "true"}
|
|
98
108
|
if uri_query:
|
|
99
109
|
parsed = parse_qs(uri_query)
|
|
110
|
+
for key, val in parsed.items():
|
|
111
|
+
params[key] = val[-1]
|
|
100
112
|
|
|
101
113
|
# Handle pages optimized for document creation.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
# from vcorelib.python import StrToBool
|
|
115
|
+
# if "print" in parsed and any(
|
|
116
|
+
# StrToBool.check(x) for x in parsed["print"]
|
|
117
|
+
# ):
|
|
118
|
+
# markdown_kwargs["bootstrap_theme"] = "light"
|
|
119
|
+
# markdown_kwargs["use_button_column"] = False
|
|
107
120
|
|
|
108
121
|
_, button_column = markdown_page(
|
|
109
122
|
document.body, markdown, **markdown_kwargs
|
|
110
123
|
)
|
|
124
|
+
button_column.add_class("border-end")
|
|
111
125
|
|
|
112
126
|
bootstrap_button(
|
|
113
127
|
icon_str("printer"),
|
|
114
128
|
tooltip="Printer-friendly view.",
|
|
115
129
|
id="print-button",
|
|
116
130
|
title="print-view button",
|
|
117
|
-
parent=div(
|
|
131
|
+
parent=div(
|
|
132
|
+
tag="a",
|
|
133
|
+
href=f"?{urlencode(params)}#light-mode",
|
|
134
|
+
parent=button_column,
|
|
135
|
+
),
|
|
118
136
|
)
|
|
119
137
|
|
|
120
138
|
# JavaScript.
|
|
@@ -8,8 +8,8 @@ from svgen.element import Element
|
|
|
8
8
|
from svgen.element.html import div
|
|
9
9
|
|
|
10
10
|
CDN = "cdn.jsdelivr.net"
|
|
11
|
-
BOOTSTRAP_VERSION = "5.3.
|
|
12
|
-
ICONS_VERSION = "1.
|
|
11
|
+
BOOTSTRAP_VERSION = "5.3.6"
|
|
12
|
+
ICONS_VERSION = "1.13.1"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def icon_str(icon: str, classes: list[str] = None) -> str:
|
|
@@ -4,7 +4,6 @@ A module for creating various bootstrap-related elements.
|
|
|
4
4
|
|
|
5
5
|
# built-in
|
|
6
6
|
from io import StringIO
|
|
7
|
-
from typing import Optional
|
|
8
7
|
|
|
9
8
|
# third-party
|
|
10
9
|
from svgen.element import Element
|
|
@@ -15,7 +14,7 @@ from vcorelib.io.file_writer import IndentedFileWriter
|
|
|
15
14
|
from runtimepy.net.html.bootstrap import icon_str
|
|
16
15
|
|
|
17
16
|
TEXT = "font-monospace"
|
|
18
|
-
BOOTSTRAP_BUTTON = f"rounded-0 {TEXT}
|
|
17
|
+
BOOTSTRAP_BUTTON = f"rounded-0 {TEXT} text-start text-nowrap"
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
def flex(kind: str = "row", **kwargs) -> Element:
|
|
@@ -26,21 +25,28 @@ def flex(kind: str = "row", **kwargs) -> Element:
|
|
|
26
25
|
return container
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
DEFAULT_PLACEMENT = "right"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def set_tooltip(
|
|
32
|
+
element: Element, data: str, placement: str = DEFAULT_PLACEMENT
|
|
33
|
+
) -> None:
|
|
30
34
|
"""Set a tooltip on an element."""
|
|
31
35
|
|
|
32
36
|
element["data-bs-title"] = data
|
|
33
37
|
element["data-bs-placement"] = placement
|
|
34
|
-
|
|
35
|
-
# Should we use another mechanism for this?
|
|
36
|
-
element["class"] += " has-tooltip"
|
|
38
|
+
element.add_class("has-tooltip")
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
BUTTON_COLOR = "secondary"
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def bootstrap_button(
|
|
43
|
-
text: str,
|
|
45
|
+
text: str,
|
|
46
|
+
tooltip: str = None,
|
|
47
|
+
color: str = BUTTON_COLOR,
|
|
48
|
+
placement: str = DEFAULT_PLACEMENT,
|
|
49
|
+
**kwargs,
|
|
44
50
|
) -> Element:
|
|
45
51
|
"""Create a bootstrap button."""
|
|
46
52
|
|
|
@@ -52,7 +58,7 @@ def bootstrap_button(
|
|
|
52
58
|
class_str=f"btn btn-{color} " + BOOTSTRAP_BUTTON,
|
|
53
59
|
)
|
|
54
60
|
if tooltip:
|
|
55
|
-
set_tooltip(button, tooltip)
|
|
61
|
+
set_tooltip(button, tooltip, placement=placement)
|
|
56
62
|
return button
|
|
57
63
|
|
|
58
64
|
|
|
@@ -66,8 +72,9 @@ def collapse_button(
|
|
|
66
72
|
"""Create a collapse button."""
|
|
67
73
|
|
|
68
74
|
collapse = bootstrap_button(icon_str(icon), tooltip=tooltip, **kwargs)
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
if target:
|
|
76
|
+
collapse["data-bs-toggle"] = toggle
|
|
77
|
+
collapse["data-bs-target"] = target
|
|
71
78
|
|
|
72
79
|
return collapse
|
|
73
80
|
|
|
@@ -75,14 +82,18 @@ def collapse_button(
|
|
|
75
82
|
def toggle_button(
|
|
76
83
|
parent: Element,
|
|
77
84
|
icon: str = "toggles",
|
|
78
|
-
title:
|
|
85
|
+
title: str = None,
|
|
79
86
|
icon_classes: list[str] = None,
|
|
80
87
|
tooltip: str = None,
|
|
88
|
+
placement: str = "top",
|
|
81
89
|
**kwargs,
|
|
82
90
|
) -> Element:
|
|
83
91
|
"""Add a boolean-toggle button."""
|
|
84
92
|
|
|
85
|
-
if title and not tooltip:
|
|
93
|
+
# if title and not tooltip:
|
|
94
|
+
if not title and tooltip:
|
|
95
|
+
kwargs["title"] = "see tooltip"
|
|
96
|
+
elif title:
|
|
86
97
|
kwargs["title"] = title
|
|
87
98
|
|
|
88
99
|
button = div(
|
|
@@ -90,31 +101,36 @@ def toggle_button(
|
|
|
90
101
|
type="button",
|
|
91
102
|
text=icon_str(icon, classes=icon_classes),
|
|
92
103
|
parent=parent,
|
|
93
|
-
class_str="btn "
|
|
104
|
+
class_str=f"btn {BOOTSTRAP_BUTTON}",
|
|
94
105
|
**kwargs,
|
|
95
106
|
)
|
|
96
107
|
if tooltip:
|
|
97
|
-
set_tooltip(button, tooltip)
|
|
108
|
+
set_tooltip(button, tooltip, placement=placement)
|
|
98
109
|
|
|
99
110
|
return button
|
|
100
111
|
|
|
101
112
|
|
|
102
113
|
def input_box(
|
|
103
114
|
parent: Element,
|
|
104
|
-
label: str = "
|
|
115
|
+
label: str = "",
|
|
105
116
|
pattern: str = ".*",
|
|
106
117
|
description: str = None,
|
|
118
|
+
placement: str = "top",
|
|
119
|
+
icon: str = "",
|
|
107
120
|
**kwargs,
|
|
108
|
-
) ->
|
|
121
|
+
) -> tuple[Element, Element, Element]:
|
|
109
122
|
"""Create command input box."""
|
|
110
123
|
|
|
111
124
|
container = div(parent=parent, class_str="input-group")
|
|
112
125
|
|
|
113
|
-
label_elem = div(tag="span", parent=container
|
|
126
|
+
label_elem = div(tag="span", parent=container)
|
|
114
127
|
label_elem.add_class("input-group-text", "rounded-0", TEXT)
|
|
115
128
|
|
|
116
129
|
if description:
|
|
117
|
-
set_tooltip(label_elem, description)
|
|
130
|
+
set_tooltip(label_elem, description, placement=placement)
|
|
131
|
+
|
|
132
|
+
if icon:
|
|
133
|
+
div(text=icon_str(icon), parent=label_elem)
|
|
118
134
|
|
|
119
135
|
box = div(
|
|
120
136
|
tag="input",
|
|
@@ -127,6 +143,8 @@ def input_box(
|
|
|
127
143
|
)
|
|
128
144
|
box.add_class("form-control", "rounded-0", TEXT)
|
|
129
145
|
|
|
146
|
+
return container, label_elem, box
|
|
147
|
+
|
|
130
148
|
|
|
131
149
|
def slider(
|
|
132
150
|
min_val: int | float, max_val: int | float, steps: int, **kwargs
|
|
@@ -180,17 +198,19 @@ def centered_markdown(
|
|
|
180
198
|
container.add_class(
|
|
181
199
|
"flex-grow-1",
|
|
182
200
|
"d-flex",
|
|
183
|
-
"flex-
|
|
201
|
+
"flex-row",
|
|
184
202
|
"justify-content-between",
|
|
185
203
|
*container_classes,
|
|
186
204
|
)
|
|
187
205
|
|
|
188
|
-
div(parent=container)
|
|
206
|
+
div(parent=container, class_str="flex-grow-1")
|
|
189
207
|
|
|
190
208
|
horiz_container = div(parent=container)
|
|
191
|
-
horiz_container.add_class(
|
|
209
|
+
horiz_container.add_class(
|
|
210
|
+
"d-flex", "flex-column", "justify-content-between"
|
|
211
|
+
)
|
|
192
212
|
|
|
193
|
-
div(parent=horiz_container)
|
|
213
|
+
div(parent=horiz_container, class_str="flex-grow-1")
|
|
194
214
|
|
|
195
215
|
with StringIO() as stream:
|
|
196
216
|
writer = IndentedFileWriter(stream)
|
|
@@ -216,8 +236,8 @@ def centered_markdown(
|
|
|
216
236
|
preformatted=True,
|
|
217
237
|
)
|
|
218
238
|
|
|
219
|
-
div(parent=horiz_container)
|
|
239
|
+
div(parent=horiz_container, class_str="flex-grow-1")
|
|
220
240
|
|
|
221
|
-
div(parent=container)
|
|
241
|
+
div(parent=container, class_str="flex-grow-1")
|
|
222
242
|
|
|
223
243
|
return container
|
|
@@ -53,9 +53,7 @@ def create_nav_container(
|
|
|
53
53
|
) -> Element:
|
|
54
54
|
"""Create a navigation container element."""
|
|
55
55
|
|
|
56
|
-
content = div(
|
|
57
|
-
id=f"{name}-{item}", role="tabpanel", tabindex="0", parent=parent
|
|
58
|
-
)
|
|
56
|
+
content = div(id=f"{name}-{item}", role="tabpanel", parent=parent)
|
|
59
57
|
content["aria-labelledby"] = f"{name}-{item}-tab"
|
|
60
58
|
|
|
61
59
|
content.add_class("tab-pane", "fade")
|
|
@@ -86,16 +84,25 @@ class TabbedContent:
|
|
|
86
84
|
self.container, self.button_column = create_app_shell(parent, id=name)
|
|
87
85
|
|
|
88
86
|
# Toggle tabs button.
|
|
89
|
-
self.add_button(
|
|
87
|
+
self.add_button(
|
|
88
|
+
"Toggle tabs",
|
|
89
|
+
f"#{PKG_NAME}-tabs",
|
|
90
|
+
id="tabs-button",
|
|
91
|
+
icon="list-task",
|
|
92
|
+
)
|
|
90
93
|
|
|
91
94
|
# Create tab container.
|
|
92
95
|
self.tabs = div(id=f"{PKG_NAME}-tabs", parent=self.container)
|
|
93
96
|
self.tabs.add_class(
|
|
94
97
|
"nav",
|
|
95
98
|
"flex-column",
|
|
99
|
+
"flex-shrink-0",
|
|
100
|
+
"flex-nowrap",
|
|
96
101
|
"nav-pills",
|
|
97
102
|
"show",
|
|
98
|
-
"
|
|
103
|
+
"h-100",
|
|
104
|
+
"overflow-y-scroll",
|
|
105
|
+
"overscroll-behavior-none",
|
|
99
106
|
)
|
|
100
107
|
|
|
101
108
|
# Create content container.
|
|
@@ -108,14 +115,14 @@ class TabbedContent:
|
|
|
108
115
|
"""Set classes on content element."""
|
|
109
116
|
|
|
110
117
|
self.content["class"] = ""
|
|
111
|
-
self.content.add_class("tab-content", "
|
|
118
|
+
self.content.add_class("tab-content", "w-100", "h-100")
|
|
112
119
|
if scroll:
|
|
113
|
-
self.content.add_class("scroll")
|
|
120
|
+
self.content.add_class("overflow-scroll")
|
|
114
121
|
|
|
115
122
|
def create(self, name: str) -> tuple[Element, Element]:
|
|
116
123
|
"""Only the first tab is active."""
|
|
117
124
|
|
|
118
|
-
container = flex(parent=self.tabs)
|
|
125
|
+
container = flex(parent=self.tabs).add_class("border-start")
|
|
119
126
|
|
|
120
127
|
# Open in new window button.
|
|
121
128
|
toggle_button(
|
|
@@ -123,9 +130,8 @@ class TabbedContent:
|
|
|
123
130
|
id=name,
|
|
124
131
|
icon="window-plus",
|
|
125
132
|
title=f"Open '{name}' in a new window.",
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
133
|
+
tabindex="-1",
|
|
134
|
+
).add_class("border-bottom", "window-button", "btn-link")
|
|
129
135
|
|
|
130
136
|
# Navigate to tab button.
|
|
131
137
|
button = create_nav_button(
|
|
@@ -133,6 +139,7 @@ class TabbedContent:
|
|
|
133
139
|
self.name,
|
|
134
140
|
name,
|
|
135
141
|
self.active_tab,
|
|
142
|
+
tabindex="-1",
|
|
136
143
|
class_str="border-bottom border-end flex-grow-1",
|
|
137
144
|
)
|
|
138
145
|
|
runtimepy/net/http/__init__.py
CHANGED
|
@@ -6,7 +6,7 @@ A module implementing an HTTP-message processing interface.
|
|
|
6
6
|
from typing import Iterator, Optional, cast
|
|
7
7
|
|
|
8
8
|
# third-party
|
|
9
|
-
from vcorelib.io import ByteFifo
|
|
9
|
+
from vcorelib.io import BinaryMessage, ByteFifo
|
|
10
10
|
|
|
11
11
|
# internal
|
|
12
12
|
from runtimepy.net.http.common import HeadersMixin
|
|
@@ -29,8 +29,8 @@ class HttpMessageProcessor:
|
|
|
29
29
|
self.current_header: Optional[HeadersMixin] = None
|
|
30
30
|
|
|
31
31
|
def ingest(
|
|
32
|
-
self, data:
|
|
33
|
-
) -> Iterator[tuple[T, Optional[
|
|
32
|
+
self, data: BinaryMessage, kind: type[T]
|
|
33
|
+
) -> Iterator[tuple[T, Optional[bytearray]]]:
|
|
34
34
|
"""Process a binary frame."""
|
|
35
35
|
|
|
36
36
|
self.buffer.ingest(data)
|