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.
Files changed (99) hide show
  1. runtimepy/__init__.py +2 -2
  2. runtimepy/channel/__init__.py +1 -4
  3. runtimepy/channel/environment/__init__.py +93 -2
  4. runtimepy/channel/environment/create.py +16 -1
  5. runtimepy/channel/environment/sample.py +10 -2
  6. runtimepy/channel/registry.py +2 -3
  7. runtimepy/codec/protocol/base.py +34 -14
  8. runtimepy/codec/protocol/json.py +5 -3
  9. runtimepy/codec/system/__init__.py +6 -2
  10. runtimepy/control/source.py +1 -1
  11. runtimepy/data/404.md +16 -0
  12. runtimepy/data/base.yaml +3 -0
  13. runtimepy/data/css/bootstrap_extra.css +59 -44
  14. runtimepy/data/css/main.css +23 -4
  15. runtimepy/data/dummy_load.yaml +5 -2
  16. runtimepy/data/factories.yaml +1 -0
  17. runtimepy/data/js/classes/App.js +54 -2
  18. runtimepy/data/js/classes/ChannelTable.js +6 -8
  19. runtimepy/data/js/classes/Plot.js +9 -4
  20. runtimepy/data/js/classes/TabFilter.js +47 -9
  21. runtimepy/data/js/classes/TabInterface.js +106 -11
  22. runtimepy/data/js/classes/WindowHashManager.js +30 -15
  23. runtimepy/data/js/init.js +18 -1
  24. runtimepy/data/js/markdown_page.js +10 -0
  25. runtimepy/data/js/sample.js +1 -0
  26. runtimepy/data/schemas/BitFields.yaml +9 -0
  27. runtimepy/data/schemas/RuntimeEnum.yaml +6 -0
  28. runtimepy/data/schemas/StructConfig.yaml +9 -1
  29. runtimepy/data/static/css/bootstrap-icons.min.css +4 -3
  30. runtimepy/data/static/css/bootstrap.min.css +3 -4
  31. runtimepy/data/static/css/fonts/bootstrap-icons.woff +0 -0
  32. runtimepy/data/static/css/fonts/bootstrap-icons.woff2 +0 -0
  33. runtimepy/data/static/js/bootstrap.bundle.min.js +5 -4
  34. runtimepy/data/static/js/webglplot.umd.min.js +2 -1
  35. runtimepy/data/static/svg/outline-dark.svg +22 -0
  36. runtimepy/data/static/svg/outline-light.svg +22 -0
  37. runtimepy/enum/__init__.py +13 -1
  38. runtimepy/enum/registry.py +13 -1
  39. runtimepy/message/__init__.py +3 -3
  40. runtimepy/mixins/logging.py +6 -1
  41. runtimepy/net/__init__.py +0 -2
  42. runtimepy/net/arbiter/info.py +36 -4
  43. runtimepy/net/arbiter/struct/__init__.py +3 -2
  44. runtimepy/net/connection.py +6 -7
  45. runtimepy/net/html/__init__.py +29 -11
  46. runtimepy/net/html/bootstrap/__init__.py +2 -2
  47. runtimepy/net/html/bootstrap/elements.py +44 -24
  48. runtimepy/net/html/bootstrap/tabs.py +18 -11
  49. runtimepy/net/http/__init__.py +3 -3
  50. runtimepy/net/http/request_target.py +3 -3
  51. runtimepy/net/mixin.py +4 -2
  52. runtimepy/net/server/__init__.py +16 -9
  53. runtimepy/net/server/app/__init__.py +1 -0
  54. runtimepy/net/server/app/create.py +3 -3
  55. runtimepy/net/server/app/env/__init__.py +30 -4
  56. runtimepy/net/server/app/env/settings.py +4 -7
  57. runtimepy/net/server/app/env/tab/base.py +2 -1
  58. runtimepy/net/server/app/env/tab/controls.py +141 -27
  59. runtimepy/net/server/app/env/tab/html.py +68 -26
  60. runtimepy/net/server/app/env/widgets.py +115 -61
  61. runtimepy/net/server/app/landing_page.py +1 -1
  62. runtimepy/net/server/app/tab.py +12 -3
  63. runtimepy/net/server/html.py +2 -2
  64. runtimepy/net/server/json.py +1 -1
  65. runtimepy/net/server/markdown.py +29 -12
  66. runtimepy/net/server/mux.py +29 -0
  67. runtimepy/net/stream/__init__.py +6 -5
  68. runtimepy/net/stream/base.py +4 -2
  69. runtimepy/net/tcp/connection.py +5 -3
  70. runtimepy/net/tcp/http/__init__.py +10 -9
  71. runtimepy/net/tcp/protocol.py +2 -2
  72. runtimepy/net/tcp/scpi/__init__.py +5 -2
  73. runtimepy/net/tcp/telnet/__init__.py +2 -1
  74. runtimepy/net/udp/connection.py +10 -6
  75. runtimepy/net/udp/protocol.py +5 -6
  76. runtimepy/net/udp/queue.py +5 -2
  77. runtimepy/net/udp/tftp/base.py +2 -1
  78. runtimepy/net/websocket/connection.py +58 -9
  79. runtimepy/primitives/array/__init__.py +7 -5
  80. runtimepy/primitives/base.py +3 -2
  81. runtimepy/primitives/field/__init__.py +35 -2
  82. runtimepy/primitives/field/fields.py +11 -2
  83. runtimepy/primitives/field/manager/base.py +19 -2
  84. runtimepy/primitives/serializable/base.py +5 -2
  85. runtimepy/primitives/serializable/fixed.py +5 -2
  86. runtimepy/primitives/serializable/prefixed.py +4 -1
  87. runtimepy/primitives/types/base.py +4 -1
  88. runtimepy/primitives/types/bounds.py +10 -4
  89. runtimepy/registry/__init__.py +20 -0
  90. runtimepy/registry/name.py +6 -0
  91. runtimepy/requirements.txt +2 -2
  92. runtimepy/ui/controls.py +20 -1
  93. {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/METADATA +6 -6
  94. {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/RECORD +98 -94
  95. {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/WHEEL +1 -1
  96. runtimepy/data/404.html +0 -7
  97. {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/entry_points.txt +0 -0
  98. {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/licenses/LICENSE +0 -0
  99. {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/top_level.txt +0 -0
@@ -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
@@ -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: bytes | str) -> None:
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: bytes) -> _Iterator[bytes]:
75
+ def process(self, data: BinaryMessage) -> _Iterator[bytearray]:
76
76
  """Process an incoming message."""
77
77
 
78
78
  self.buffer.ingest(data)
@@ -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",
@@ -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, _ABC):
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(self.byte_order, **kwargs)
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.array = self.env.array(byte_order=byte_order, **kwargs).array
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: bytes) -> Iterator[int]:
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: bytes, addr: tuple[str, int]
194
+ self, data: BinaryMessage, addr: tuple[str, int]
194
195
  ) -> bool:
195
196
  """Process an array of struct instances."""
196
197
 
@@ -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: bytes) -> bool:
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.raw.value += 1
253
+ self._restarts.increment()
255
254
 
256
- self._restart_attempts.raw.value += 1
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: bytes) -> bool:
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: bytes) -> bool:
406
+ async def process_binary(self, data: BinaryMessage) -> bool:
408
407
  """Process a binary frame."""
409
408
  return True
@@ -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(parent=container if use_button_column else None)
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("lightbulb"),
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
- if "print" in parsed and any(
103
- StrToBool.check(x) for x in parsed["print"]
104
- ):
105
- markdown_kwargs["bootstrap_theme"] = "light"
106
- markdown_kwargs["use_button_column"] = False
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(tag="a", href="?print=true", parent=button_column),
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.3"
12
- ICONS_VERSION = "1.11.3"
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} button-bodge text-nowrap"
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
- def set_tooltip(element: Element, data: str, placement: str = "right") -> None:
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, tooltip: str = None, color: str = BUTTON_COLOR, **kwargs
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
- collapse["data-bs-toggle"] = toggle
70
- collapse["data-bs-target"] = target
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: Optional[str] = "toggle value",
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 " + BOOTSTRAP_BUTTON,
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 = "filter",
115
+ label: str = "",
105
116
  pattern: str = ".*",
106
117
  description: str = None,
118
+ placement: str = "top",
119
+ icon: str = "",
107
120
  **kwargs,
108
- ) -> None:
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, text=label)
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-column",
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("d-flex", "flex-row", "justify-content-between")
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("Toggle tabs", f"#{PKG_NAME}-tabs", id="tabs-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
- "flex-column-scroll-bodge",
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", "tab-content-bodge")
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
- ).add_class(
127
- "border-start", "border-bottom", "window-button", "btn-link"
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
 
@@ -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: bytes, kind: type[T]
33
- ) -> Iterator[tuple[T, Optional[bytes]]]:
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)