runtimepy 5.13.2__py3-none-any.whl → 5.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
runtimepy/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # =====================================
2
2
  # generator=datazen
3
3
  # version=3.2.3
4
- # hash=6693eae62108cbcdfbccdb9de6b57fa8
4
+ # hash=c6b2bd8ab0196f119c02bcdf6807ecab
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.13.2"
13
+ VERSION = "5.14.0"
14
14
 
15
15
  # runtimepy-specific content.
16
16
  METRICS_NAME = "metrics"
@@ -53,3 +53,8 @@ class ProtocolFactory(ABC):
53
53
  if cls._singleton is None:
54
54
  cls._singleton = cls.instance()
55
55
  return cls._singleton
56
+
57
+ @classmethod
58
+ def id(cls) -> int:
59
+ """Get an integer identifier for this protocol factory."""
60
+ return cls.singleton().id
@@ -6,6 +6,7 @@ A module implementing an interface to build communication protocols.
6
6
  from contextlib import contextmanager, suppress
7
7
  from copy import copy as _copy
8
8
  from io import StringIO
9
+ from typing import BinaryIO as _BinaryIO
9
10
  from typing import Iterator as _Iterator
10
11
  from typing import NamedTuple
11
12
  from typing import Optional as _Optional
@@ -18,8 +19,9 @@ from vcorelib.logging import LoggerType
18
19
 
19
20
  # internal
20
21
  from runtimepy.enum import RuntimeEnum as _RuntimeEnum
21
- from runtimepy.enum.registry import EnumRegistry as _EnumRegistry
22
+ from runtimepy.enum.registry import DEFAULT_ENUM_PRIMITIVE, EnumRegistry
22
23
  from runtimepy.primitives import AnyPrimitive as _AnyPrimitive
24
+ from runtimepy.primitives import PrimitiveInstancelike
23
25
  from runtimepy.primitives import Primitivelike as _Primitivelike
24
26
  from runtimepy.primitives import normalize_instance as _normalize_instance
25
27
  from runtimepy.primitives.array import PrimitiveArray
@@ -29,6 +31,7 @@ from runtimepy.primitives.byte_order import (
29
31
  from runtimepy.primitives.byte_order import ByteOrder as _ByteOrder
30
32
  from runtimepy.primitives.field.fields import BitFields as _BitFields
31
33
  from runtimepy.primitives.field.manager import BitFieldsManager
34
+ from runtimepy.primitives.int import UnsignedInt
32
35
  from runtimepy.primitives.serializable import Serializable, SerializableMap
33
36
  from runtimepy.registry.name import NameRegistry as _NameRegistry
34
37
  from runtimepy.registry.name import RegistryKey as _RegistryKey
@@ -66,11 +69,12 @@ class ProtocolBase(PrimitiveArray):
66
69
 
67
70
  def __init__(
68
71
  self,
69
- enum_registry: _EnumRegistry,
72
+ enum_registry: EnumRegistry,
70
73
  names: _NameRegistry = None,
71
74
  fields: BitFieldsManager = None,
72
75
  build: ProtocolBuild = None,
73
76
  identifier: int = 1,
77
+ identifier_primitive: PrimitiveInstancelike = DEFAULT_ENUM_PRIMITIVE,
74
78
  byte_order: _Union[_ByteOrder, _RegistryKey] = _DEFAULT_BYTE_ORDER,
75
79
  serializables: SerializableMap = None,
76
80
  alias: str = None,
@@ -78,6 +82,12 @@ class ProtocolBase(PrimitiveArray):
78
82
  """Initialize this protocol."""
79
83
 
80
84
  self.id = identifier
85
+ self.id_primitive: UnsignedInt = _normalize_instance( # type: ignore
86
+ identifier_primitive
87
+ )
88
+ assert self.id_primitive.kind.is_integer
89
+ self.id_primitive.value = self.id
90
+
81
91
  self.alias = alias
82
92
 
83
93
  # Register the byte-order enumeration if it's not present.
@@ -150,6 +160,7 @@ class ProtocolBase(PrimitiveArray):
150
160
  build=self._build,
151
161
  byte_order=self.byte_order,
152
162
  identifier=self.id,
163
+ identifier_primitive=self.id_primitive.kind.name,
153
164
  serializables={
154
165
  key: [val[0].copy_without_chain()]
155
166
  for key, val in self.serializables.items()
@@ -331,3 +342,8 @@ class ProtocolBase(PrimitiveArray):
331
342
  def __setitem__(self, name: str, val: ProtocolPrimitive) -> None:
332
343
  """Set a value of a field belonging to the protocol."""
333
344
  self.set(name, val)
345
+
346
+ def write_with_id(self, stream: _BinaryIO) -> None:
347
+ """Write this instance to a stream with its identifier as a prefix."""
348
+ self.id_primitive.to_stream(stream, byte_order=self.byte_order)
349
+ self.to_stream(stream)
@@ -54,6 +54,7 @@ class JsonProtocol(ProtocolBase):
54
54
 
55
55
  data[META_KEY] = {
56
56
  "id": self.id,
57
+ "id_primitive": self.id_primitive.kind.name,
57
58
  "byte_order": self.byte_order.name.lower(),
58
59
  "alias": self.alias,
59
60
  }
@@ -131,6 +132,7 @@ class JsonProtocol(ProtocolBase):
131
132
  fields=fields,
132
133
  build=build,
133
134
  identifier=_cast(int, data[META_KEY]["id"]),
135
+ identifier_primitive=_cast(str, data[META_KEY]["id_primitive"]),
134
136
  byte_order=_cast(str, data[META_KEY]["byte_order"]),
135
137
  alias=data[META_KEY]["alias"], # type: ignore
136
138
  serializables=serializables,
@@ -0,0 +1,91 @@
1
+ """
2
+ A module implementing an interface for receiving struct messages.
3
+ """
4
+
5
+ # built-in
6
+ from io import BytesIO
7
+ import os
8
+ from typing import Callable
9
+
10
+ # third-party
11
+ from vcorelib.logging import LoggerMixin
12
+
13
+ # internal
14
+ from runtimepy.codec.protocol import Protocol, ProtocolFactory
15
+ from runtimepy.primitives.byte_order import ByteOrder
16
+ from runtimepy.primitives.int import UnsignedInt
17
+
18
+ StructHandler = Callable[[Protocol], None]
19
+
20
+
21
+ class StructReceiver(LoggerMixin):
22
+ """A class for sending and receiving struct messages."""
23
+
24
+ id_primitive: UnsignedInt
25
+ byte_order: ByteOrder
26
+
27
+ def __init__(self, *factories: type[ProtocolFactory]) -> None:
28
+ """Initialize this instance."""
29
+
30
+ super().__init__()
31
+
32
+ self.handlers: dict[int, StructHandler] = {}
33
+ self.instances: dict[int, Protocol] = {}
34
+ for factory in factories:
35
+ self.register(factory)
36
+
37
+ def add_handler(self, identifier: int, handler: StructHandler) -> None:
38
+ """Add a struct message handler."""
39
+
40
+ assert identifier not in self.handlers
41
+ self.handlers[identifier] = handler
42
+
43
+ def register(self, factory: type[ProtocolFactory]) -> None:
44
+ """Track a protocol factory's structure by identifier."""
45
+
46
+ inst = factory.singleton()
47
+
48
+ if not hasattr(self, "id_primitive"):
49
+ self.id_primitive = inst.id_primitive.copy() # type: ignore
50
+ self.byte_order = inst.byte_order
51
+ else:
52
+ assert self.id_primitive.kind == inst.id_primitive.kind
53
+ assert self.byte_order == inst.byte_order
54
+
55
+ assert inst.id not in self.instances
56
+ self.instances[inst.id] = inst
57
+
58
+ def process(self, data: bytes) -> None:
59
+ """Attempt to process a struct message."""
60
+
61
+ with BytesIO(data) as stream:
62
+ stream.seek(0, os.SEEK_END)
63
+ end_pos = stream.tell()
64
+ stream.seek(0, os.SEEK_SET)
65
+
66
+ while stream.tell() < end_pos:
67
+ ident = self.id_primitive.from_stream(
68
+ stream, byte_order=self.byte_order
69
+ )
70
+ if ident in self.instances:
71
+ inst = self.instances[ident]
72
+ inst.from_stream(stream)
73
+ if ident in self.handlers:
74
+ self.handlers[ident](inst)
75
+ else:
76
+ self.logger.warning(
77
+ "No message handler for struct '%d' (%s).",
78
+ ident,
79
+ inst,
80
+ )
81
+
82
+ # Can't continue reading if we don't know this identifier.
83
+ else:
84
+ self.logger.error(
85
+ "Unknown struct identifier '%d' "
86
+ "@%d/%d of stream (aborting).",
87
+ ident,
88
+ stream.tell(),
89
+ end_pos,
90
+ )
91
+ stream.seek(0, os.SEEK_END)
runtimepy/commands/all.py CHANGED
@@ -1,17 +1,13 @@
1
1
  # =====================================
2
2
  # generator=datazen
3
3
  # version=3.2.3
4
- # hash=1814b7f7fae3556a0dbeec37141e4182
4
+ # hash=f4667fb34b5066e16f0621961f11efd8
5
5
  # =====================================
6
6
 
7
7
  """
8
8
  A module aggregating package commands.
9
9
  """
10
10
 
11
- # built-in
12
- from typing import List as _List
13
- from typing import Tuple as _Tuple
14
-
15
11
  # third-party
16
12
  from vcorelib.args import CommandRegister as _CommandRegister
17
13
 
@@ -24,7 +20,7 @@ from runtimepy.commands.tftp import add_tftp_cmd
24
20
  from runtimepy.commands.tui import add_tui_cmd
25
21
 
26
22
 
27
- def commands() -> _List[_Tuple[str, str, _CommandRegister]]:
23
+ def commands() -> list[tuple[str, str, _CommandRegister]]:
28
24
  """Get this package's commands."""
29
25
 
30
26
  return [
runtimepy/data/404.html CHANGED
@@ -1,6 +1,7 @@
1
- <html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>runtimepy/5.12.0</title><style>@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-Regular.woff2)}@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-Bold.woff2);font-weight:700}@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-BoldItalic.woff2);font-weight:700;font-style:italic}@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-Italic.woff2);font-style:italic}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-Regular.woff2)}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-Bold.woff2);font-weight:700}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-BoldItalic.woff2);font-weight:700;font-style:italic}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-Italic.woff2);font-style:italic}</style><link rel=stylesheet href=/static/css/bootstrap-icons.min.css></link>
2
- <link href=/static/css/bootstrap.min.css rel=stylesheet crossorigin=anonymous></link><style>html{height:100%;width:100%;position:fixed}body{height:100%;margin:0;overflow:hidden}body>:first-child{height:100%}#runtimepy{height:100%}#runtimepy-tabs{width:min-content}#runtimepy-splash{position:fixed;top:0;left:0;width:100vw;height:100vh;opacity:1}.click-plot{cursor:pointer}.stale{color:var(--bs-warning-text-emphasis)!important}.slider{min-width:8em}.toggle-value{}.window-button{border-right:var(--bs-border-width)var(--bs-border-style)var(--bs-link-hover-color)!important}canvas:focus{outline:none}.flex-column-scroll-bodge{height:100%;flex-wrap:nowrap;overflow-y:scroll;flex-shrink:0}.scroll{overflow:scroll}.tab-content-bodge{width:100%;height:100%}.button-bodge{text-align:left}.collapsing{transition:none!important}.tab-pane.fade{transition:none!important}.modal.fade{transition:none!important}.modal-dialog{width:80%;max-width:80%}.table{margin-bottom:0;width:auto}.table-container{overflow-x:scroll;min-height:fit-content}.channel-column{overflow-y:scroll;overflow-x:hidden;flex-grow:0;flex-shrink:0;max-width:75%}.channel-value{min-width:10em}.table>tbody>tr>td{vertical-align:middle}.collapse:not(.show){display:none!important}select.form-select{width:min-content}select.form-select:hover{cursor:pointer}textarea.text-logs{min-height:10em!important;padding-top:0!important;padding-bottom:0!important;border-top:0}.vertical-divider{flex-basis:.75em;flex-grow:0;flex-shrink:0}pre{color:var(--bs-code-color)}.vertical-divider:hover{cursor:col-resize;background-color:var(--bs-highlight-bg)!important}button:hover{background-color:var(--bs-tertiary-bg)}.channel-value-input{width:6em}:root,[data-bs-theme=dark],[data-bs-theme=light]{--bs-font-sans-serif:CascadiaCode, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--bs-font-monospace:CascadiaMono, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}</style><div id=runtimepy class="align-items-start d-flex bg-body" data-bs-theme=dark><div class="d-flex h-100 bg-dark-subtle flex-column"><button type=button id=theme-button class="btn btn-secondary rounded-0 font-monospace button-bodge text-nowrap has-tooltip" data-bs-title=" Toggle light/dark." data-bs-placement=right>
1
+ <!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Libre Embedded - Home
2
+ </title><meta http-equiv=Cache-Control content="public"><link rel=icon href=/favicon.ico><meta name=description content="Libre Embedded Engineering Technologies LLC is an engineering consultancy and product development company. This page was rendered from Markdown by runtimepy/5.13.3."><style>@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-Regular.woff2)}@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-Bold.woff2);font-weight:700}@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-BoldItalic.woff2);font-weight:700;font-style:italic}@font-face{font-family:CascadiaCode;src:url(/static/woff2/CascadiaCode-Italic.woff2);font-style:italic}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-Regular.woff2)}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-Bold.woff2);font-weight:700}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-BoldItalic.woff2);font-weight:700;font-style:italic}@font-face{font-family:CascadiaMono;src:url(/static/woff2/CascadiaMono-Italic.woff2);font-style:italic}</style><link rel=stylesheet href=/static/css/bootstrap-icons.min.css></link>
3
+ <link href=/static/css/bootstrap.min.css rel=stylesheet crossorigin=anonymous></link><style>html{height:100%;width:100%;position:fixed}body{height:100%;margin:0;overflow:hidden}body>:first-child{height:100%}#runtimepy{height:100%}#runtimepy-tabs{width:min-content}#runtimepy-splash{position:fixed;top:0;left:0;width:100vw;height:100vh;opacity:1}.click-plot{cursor:pointer}.stale{color:var(--bs-warning-text-emphasis)!important}.slider{min-width:8em}.toggle-value{}.window-button{border-right:var(--bs-border-width)var(--bs-border-style)var(--bs-link-hover-color)!important}:focus{outline:none}.flex-column-scroll-bodge{height:100%;flex-wrap:nowrap;overflow-y:scroll;flex-shrink:0}.scroll{overflow:scroll}.tab-content-bodge{width:100%;height:100%}.button-bodge{text-align:left}.collapsing{transition:none!important}.tab-pane.fade{transition:none!important}.modal.fade{transition:none!important}.modal-dialog{width:80%;max-width:80%}.table{margin-bottom:0;width:auto}.table-container{overflow-x:scroll;min-height:fit-content}.channel-column{overflow-y:scroll;overflow-x:hidden;flex-grow:0;flex-shrink:0;max-width:75%}.channel-value{min-width:10em}.table>tbody>tr>td{vertical-align:middle}.collapse:not(.show){display:none!important}select.form-select{width:min-content}select.form-select:hover{cursor:pointer}textarea.text-logs{min-height:10em!important;padding-top:0!important;padding-bottom:0!important;border-top:0}.vertical-divider{flex-basis:.75em;flex-grow:0;flex-shrink:0}pre{color:var(--bs-code-color)}.vertical-divider:hover{cursor:col-resize;background-color:var(--bs-highlight-bg)!important}button:hover{background-color:var(--bs-tertiary-bg)}.channel-value-input{width:6em}:root,[data-bs-theme=dark],[data-bs-theme=light]{--bs-font-sans-serif:CascadiaCode, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--bs-font-monospace:CascadiaMono, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}</style><div id=runtimepy class="d-flex bg-body align-items-start" data-bs-theme=dark><div class="bg-dark-subtle d-flex h-100 flex-column"><button type=button id=theme-button title="change theme button" class="btn btn-secondary rounded-0 font-monospace button-bodge text-nowrap has-tooltip" data-bs-title="Toggle light/dark." data-bs-placement=right>
3
4
  <i class="bi bi-lightbulb"></i>
4
5
  </button>
5
- <a href="?print=true"><button type=button id=print-button class="btn btn-secondary rounded-0 font-monospace button-bodge text-nowrap has-tooltip" data-bs-title="Printer-friendly view." data-bs-placement=right>
6
- <i class="bi bi-printer"></i></button></a></div><div class="d-flex h-100 justify-content-between flex-grow-1 flex-column overflow-y-auto text-body"><div></div><div class="d-flex flex-row justify-content-between"><div></div><div class="text-body p-3 pb-0"><h1><a href=/><img alt=logo src=/static/png/chip-circle-bootstrap/128x128.png></a> Resource Not Found (404) <a href=/><img alt=logo src=/static/png/chip-circle-bootstrap/128x128.png></a></h1><p>(<a href=/>home</a>)</div><div></div></div><div></div></div></div><script>function worker_config(e){let t={},s="runtimepy_websocket",n="ws";location.protocol.includes("https")&&(s="runtimepy_secure_websocket",n+="s");let o=e.config.ports;for(let r in o){let i=o[r],a=e.config.websocket_hostname||window.location.hostname;i.name.includes(s)&&(i.name.includes("data")?t.data=`${n}://${a}:`+i.port:t.json=`${n}://${a}:`+i.port)}return t}let tabFilter=void 0;function bootstrap_init(){const t=document.querySelectorAll(".has-tooltip"),n=[...t].map(e=>new bootstrap.Tooltip(e));let e=document.getElementById("runtimepy-tabs");e&&(tabFilter=new TabFilter(e))}let lightMode=!1;function lightDarkClick(){lightMode=!lightMode,document.getElementById("runtimepy").setAttribute("data-bs-theme",lightMode?"light":"dark"),window.location.hash=lightMode?"#light-mode":""}window.onload=()=>{let e=document.getElementById("theme-button");if(e&&e.addEventListener("click",lightDarkClick),window.location.hash){let t=window.location.hash.slice(1).split(",");t.includes("light-mode")&&e.click()}bootstrap_init()}</script><script src=/static/js/bootstrap.bundle.min.js crossorigin=anonymous></script>
6
+ <a href="?print=true"><button type=button id=print-button title="print-view button" class="btn btn-secondary rounded-0 font-monospace button-bodge text-nowrap has-tooltip" data-bs-title="Printer-friendly view." data-bs-placement=right>
7
+ <i class="bi bi-printer"></i></button></a></div><div class="h-100 flex-grow-1 justify-content-between text-body d-flex overflow-y-auto flex-column"><div></div><div class="justify-content-between d-flex flex-row"><div></div><div class="text-body p-3 pb-0"><h1><a href=/><img alt=logo src=/static/png/chip-circle-bootstrap/128x128.png></a> Resource Not Found (404) <a href=/><img alt=logo src=/static/png/chip-circle-bootstrap/128x128.png></a></h1><p>(<a href=/>home</a>)</div><div></div></div><div></div></div></div><script>function worker_config(e){let t={},s="runtimepy_websocket",n="ws";location.protocol.includes("https")&&(s="runtimepy_secure_websocket",n+="s");let o=e.config.ports;for(let r in o){let i=o[r],a=e.config.websocket_hostname||window.location.hostname;i.name.includes(s)&&(i.name.includes("data")?t.data=`${n}://${a}:`+i.port:t.json=`${n}://${a}:`+i.port)}return t}let tabFilter=void 0;function bootstrap_init(){const t=document.querySelectorAll(".has-tooltip"),n=[...t].map(e=>new bootstrap.Tooltip(e));let e=document.getElementById("runtimepy-tabs");e&&(tabFilter=new TabFilter(e))}let lightMode=!1;function lightDarkClick(){lightMode=!lightMode,document.getElementById("runtimepy").setAttribute("data-bs-theme",lightMode?"light":"dark"),window.location.hash=lightMode?"#light-mode":""}window.onload=()=>{let e=document.getElementById("theme-button");if(e&&e.addEventListener("click",lightDarkClick),window.location.hash){let t=window.location.hash.slice(1).split(",");t.includes("light-mode")&&e.click()}bootstrap_init()}</script><script src=/static/js/bootstrap.bundle.min.js crossorigin=anonymous></script>
runtimepy/entry.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # =====================================
2
2
  # generator=datazen
3
3
  # version=3.2.3
4
- # hash=79c31d1280a6e97b5d326aecb758c597
4
+ # hash=17cbf063ff27b9146abd2014891778a2
5
5
  # =====================================
6
6
 
7
7
  """
@@ -14,7 +14,6 @@ from logging import getLogger
14
14
  import os
15
15
  from pathlib import Path
16
16
  import sys
17
- from typing import List
18
17
 
19
18
  # third-party
20
19
  from vcorelib.logging import init_logging, log_time, logging_args
@@ -24,7 +23,7 @@ from runtimepy import DESCRIPTION, VERSION
24
23
  from runtimepy.app import add_app_args, entry
25
24
 
26
25
 
27
- def main(argv: List[str] = None) -> int:
26
+ def main(argv: list[str] = None) -> int:
28
27
  """Program entry-point."""
29
28
 
30
29
  result = 0
@@ -88,6 +88,13 @@ class ResponseHeader(HeadersMixin):
88
88
  self.headers,
89
89
  )
90
90
 
91
+ def static_resource(
92
+ self, value: str = "public, max-age=31536000, immutable"
93
+ ) -> None:
94
+ """Set headers for static resources."""
95
+
96
+ self["Cache-Control"] = value
97
+
91
98
 
92
99
  class AsyncResponse:
93
100
  """
@@ -237,6 +237,8 @@ class RuntimepyServerConnection(HttpConnection):
237
237
 
238
238
  self.logger.info("Serving '%s' (MIME: %s)", candidate, mime)
239
239
 
240
+ response.static_resource()
241
+
240
242
  # Return the file data.
241
243
  result = AsyncResponse(candidate)
242
244
  break
@@ -321,6 +323,7 @@ class RuntimepyServerConnection(HttpConnection):
321
323
  # Handle favicon (for browser clients).
322
324
  if path.startswith("/favicon"):
323
325
  response["Content-Type"] = "image/x-icon"
326
+ response.static_resource()
324
327
  return self.favicon_data
325
328
 
326
329
  # Try serving a file and handling redirects.
@@ -68,6 +68,12 @@ class HttpConnection(_TcpConnection):
68
68
  # runtime don't need additional initialization.
69
69
  handlers: HttpRequestHandlers = {}
70
70
 
71
+ headers: dict[str, str] = {
72
+ "Server": PKG_NAME,
73
+ "X-Content-Type-Options": "nosniff",
74
+ "Cache-Control": "public",
75
+ }
76
+
71
77
  def init(self) -> None:
72
78
  """Initialize this instance."""
73
79
 
@@ -128,12 +134,9 @@ class HttpConnection(_TcpConnection):
128
134
  f"No handler for {request_header.method} requests."
129
135
  )
130
136
 
131
- # Set boilerplate header data.
132
-
133
- # webhint suggestions
134
- # response["server"] = self.identity
135
- response["server"] = PKG_NAME
136
- response["X-Content-Type-Options"] = "nosniff"
137
+ for key, value in type(self).headers.items():
138
+ if response.get(key) is None:
139
+ response[key] = value
137
140
 
138
141
  return result
139
142
 
@@ -121,9 +121,10 @@ def create(value: Primitivelike, **kwargs) -> AnyPrimitive:
121
121
  return normalize(value)(**kwargs)
122
122
 
123
123
 
124
- def normalize_instance(
125
- value: Primitivelike | AnyPrimitive, **kwargs
126
- ) -> AnyPrimitive:
124
+ PrimitiveInstancelike = _Union[Primitivelike, AnyPrimitive]
125
+
126
+
127
+ def normalize_instance(value: PrimitiveInstancelike, **kwargs) -> AnyPrimitive:
127
128
  """Creates a new instance only if necessary."""
128
129
 
129
130
  if not isinstance(value, Primitive):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtimepy
3
- Version: 5.13.2
3
+ Version: 5.14.0
4
4
  Summary: A framework for implementing Python services.
5
5
  Home-page: https://github.com/libre-embedded/runtimepy
6
6
  Author: Libre Embedded
@@ -17,11 +17,11 @@ Classifier: Development Status :: 5 - Production/Stable
17
17
  Requires-Python: >=3.12
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: aiofiles
21
20
  Requires-Dist: vcorelib>=3.5.8
22
21
  Requires-Dist: psutil
23
- Requires-Dist: svgen>=0.7.12
24
22
  Requires-Dist: websockets
23
+ Requires-Dist: aiofiles
24
+ Requires-Dist: svgen>=0.7.12
25
25
  Provides-Extra: test
26
26
  Requires-Dist: pylint; extra == "test"
27
27
  Requires-Dist: flake8; extra == "test"
@@ -51,11 +51,11 @@ Dynamic: requires-python
51
51
  =====================================
52
52
  generator=datazen
53
53
  version=3.2.3
54
- hash=f3bcab8765e649f1b80ad504e5debde9
54
+ hash=f46a9eb2c8979cc9ff0a3444077956ec
55
55
  =====================================
56
56
  -->
57
57
 
58
- # runtimepy ([5.13.2](https://pypi.org/project/runtimepy/))
58
+ # runtimepy ([5.14.0](https://pypi.org/project/runtimepy/))
59
59
 
60
60
  [![python](https://img.shields.io/pypi/pyversions/runtimepy.svg)](https://pypi.org/project/runtimepy/)
61
61
  ![Build Status](https://github.com/libre-embedded/runtimepy/workflows/Python%20Package/badge.svg)
@@ -1,8 +1,8 @@
1
- runtimepy/__init__.py,sha256=SS-yzin_UgsO_Yr3uFKIDgmWSOp6OCzrOj3rb2DZ0Qo,391
1
+ runtimepy/__init__.py,sha256=QTcIVQhNPp8R5ZdjWYD99HzoCmAlfUyTO-OLPwSonys,391
2
2
  runtimepy/__main__.py,sha256=IKioH2xOtsXwrwb9zABDQEJvuAX--Lnh84TeSz0XSs0,332
3
3
  runtimepy/app.py,sha256=Er1ZKKrG9U0FV0gQg_GYF9xDb89HgYnVzS5SjxGa2Tg,970
4
4
  runtimepy/dev_requirements.txt,sha256=VZhW6bJ5YbwaoN4d_XxZFuN5BbDLaG7ngKrGnugVPRw,245
5
- runtimepy/entry.py,sha256=DGHLv_SmTMImRcWjbVa2dKiWkhvAbxZwuvtvWLbx5U4,1954
5
+ runtimepy/entry.py,sha256=fy9irGEgyFAsu868bOymHX8I3CkkrMNThUWAm-4chiM,1930
6
6
  runtimepy/mapping.py,sha256=VQK1vzmQVvYYKI85_II37-hIEbvgL3PzNy-WI6TTo80,5091
7
7
  runtimepy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  runtimepy/requirements.txt,sha256=uc8eW9HzoBsDmspEvyDKIs-xGrWs_UUltwAQCk072PU,125
@@ -24,12 +24,13 @@ runtimepy/channel/environment/command/result.py,sha256=Ko5lK4d04Z266WuCi2sHQItbU
24
24
  runtimepy/channel/event/__init__.py,sha256=9LCSNa1Iiwr6Q6JkwQGELDQ7rWfU_xjAMh6qM1I-ueM,2793
25
25
  runtimepy/channel/event/header.py,sha256=eDRZgzzM5HZQ8QtV4DjyAsrAhEZyM7IfwqK6WB5ACEw,868
26
26
  runtimepy/codec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- runtimepy/codec/protocol/__init__.py,sha256=Rg7RSKGn2UBpGMpwq1aCLUBA5h4pORdh53NfR7Cjw0U,1530
28
- runtimepy/codec/protocol/base.py,sha256=ezNX-93NBChNpMszkkcx-0X_YzdU2oOFdqAMfKaEqMo,11053
29
- runtimepy/codec/protocol/json.py,sha256=qmcoCcTRS-HgVYLPBsCAPhekAgZmHX9JBfGivLl3b0Y,4349
27
+ runtimepy/codec/protocol/__init__.py,sha256=0mdPIb5mpkqofClqW23kslsgc3V_aSKJ1RmjEHmKfeI,1673
28
+ runtimepy/codec/protocol/base.py,sha256=8pt8va16zHcHIgFTyhfx6tgDcsQxbqL93CfSgq0ZTBY,11796
29
+ runtimepy/codec/protocol/json.py,sha256=pDEpHYFNYjiU2whvQemdZmH9Igx7aLEYCoS_Xpnqdcs,4483
30
+ runtimepy/codec/protocol/receiver.py,sha256=67fH90YY3bgMzF-aqsyUvpIF77qxaMX3NJVwCC4BgKc,2976
30
31
  runtimepy/codec/system/__init__.py,sha256=fIOUo7QhwI81YAIz9myeSo1oo94De41sK5HKJ-sAdfY,7959
31
32
  runtimepy/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- runtimepy/commands/all.py,sha256=WfKC5W8a5RzgE24QucynDvsOEcWO1e0Akm2etx90uFQ,1619
33
+ runtimepy/commands/all.py,sha256=eAn52vuAgOh0jr93LROfbTjSPuJILGFRCPB9UeS3Q1c,1537
33
34
  runtimepy/commands/arbiter.py,sha256=CtTMRYpqCAN3vWHkkr9jqWpoF7JGNXafKIBFmkarAfc,1567
34
35
  runtimepy/commands/common.py,sha256=NvZdeIFBHAF52c1n7vqD59DW6ywc-rG5iC5MpuhGf-c,2449
35
36
  runtimepy/commands/mtu.py,sha256=LFFjTU4SsuV3j7Mhx_WuKa5lfdfMm70zJvDWToVrP7E,1357
@@ -41,7 +42,7 @@ runtimepy/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
41
42
  runtimepy/control/source.py,sha256=nW3Q2D-LcekII7K5XKbxXCcR-9jYQyvv0UypeNy1Dnw,1695
42
43
  runtimepy/control/step.py,sha256=2LdZTpMHLwHLdpPVinpC2qByTs5I5LTDt-xONn_6Fc8,5491
43
44
  runtimepy/control/env/__init__.py,sha256=RHJqysY7Pv4VDs2SGk0X-qc5xp_SrQ_oxb5Deug8HEM,1339
44
- runtimepy/data/404.html,sha256=D_nhuj1AfO5EaIVsajknfaeZ5cVDKCNRPtUY4MHg1Xc,5326
45
+ runtimepy/data/404.html,sha256=c_cIqG5BxcKw7pDHcgAAPeOdQq1JRAWIBZ331tG9LKw,5673
45
46
  runtimepy/data/base.yaml,sha256=WGy2nPiRZk5_wBU7BlWaFfCnzRprJBp-m8onOPUoLAo,399
46
47
  runtimepy/data/browser.yaml,sha256=oc5KEV1C1uAJ4MkhNo4hyVVfJtZvHelRNqzNvD313Ow,79
47
48
  runtimepy/data/dummy_load.yaml,sha256=PfKRXXgZnENRMSd68eznSMTV8xanVH5JY4FmoZRPFGY,1985
@@ -191,10 +192,10 @@ runtimepy/net/http/__init__.py,sha256=4TjFp_ajAVcOEvwtjlF6mG-9EbEePqFZht-QpWIKVB
191
192
  runtimepy/net/http/common.py,sha256=vpoO6XwRmrZmTkCu9bkI0HnyaD8MWTpV7ADesCNrfRE,2237
192
193
  runtimepy/net/http/header.py,sha256=AECSdvhBA9_5Pg3UdwMzsmBpcqgsiPj41xnIGPm5g5E,2296
193
194
  runtimepy/net/http/request_target.py,sha256=EE1aI5VSARw1h93jyZvP56ir5O5fjd6orYK-1FM4JNk,1550
194
- runtimepy/net/http/response.py,sha256=y33KLUJNE7zx6biUMwTkUfQ1bXiKV6pjrcxuh8U3WCE,3216
195
+ runtimepy/net/http/response.py,sha256=fD0R_BUgmNwdwKQtXvYfTYM7DyJlwsGmlNVi5HkCOhU,3409
195
196
  runtimepy/net/http/state.py,sha256=qCMN8aWfCRfU9XP-cIhSOo2RqfljTjbQRCflfcy2bfY,1626
196
197
  runtimepy/net/http/version.py,sha256=mp6rgIM7-VUVKLCA0Uw96CmBkL0ET860lDVVEewpZ7w,1098
197
- runtimepy/net/server/__init__.py,sha256=LqN4NSkk_B0sv-i1PMv98kzkMGCCVbAPTxst1Xgjq6Y,11340
198
+ runtimepy/net/server/__init__.py,sha256=prEf7pNhM7t9xP7_ZM0uzESUomlZuqDtDlE34MnyCb0,11431
198
199
  runtimepy/net/server/html.py,sha256=TZ2ZSPTxgA6KAKS26tNrSUpxFUrdk92K5zP2ybcSYqs,1892
199
200
  runtimepy/net/server/json.py,sha256=a7vM5yfq2er4DexzFqEMnxoMGDeuywKkVH4-uNJBAik,2522
200
201
  runtimepy/net/server/markdown.py,sha256=3kLQR3fsGFU8dUiDuMbUiLv7XKx9s4zXy9SZsJDDc9Y,1507
@@ -228,7 +229,7 @@ runtimepy/net/tcp/__init__.py,sha256=OOWohegpoioSTf8M7uDf-4EV1IDungz7-U19L_2yW4I
228
229
  runtimepy/net/tcp/connection.py,sha256=sYWj2aDiAHQf70zfRJM24cHraFd_TuzTD4fRWeSQZXE,8811
229
230
  runtimepy/net/tcp/create.py,sha256=zZsRs5KYpO3bNGh-DwEOEzjUDE4ixj-UBHYgZ0GvC7c,2013
230
231
  runtimepy/net/tcp/protocol.py,sha256=vEnIX3gUX2nrw9ofT_e4KYU4VY2k4WP0WuOi4eE_OOQ,1444
231
- runtimepy/net/tcp/http/__init__.py,sha256=nXgUoC7jKlR7Ay7cTkomU2u_TcTsoO38uastZSBGa9k,6228
232
+ runtimepy/net/tcp/http/__init__.py,sha256=-ODo-dJcyD-NZnRIFoWT9JdQnDaP_cXJ9IViwXIQe24,6301
232
233
  runtimepy/net/tcp/scpi/__init__.py,sha256=aWCWQfdeyfoU9bpOnOtyIQbT1swl4ergXLFn5kXAH28,2105
233
234
  runtimepy/net/tcp/telnet/__init__.py,sha256=96eJFb301I3H2ivDtGMQtDDw09Xm5NRvM9VEC-wjt8c,4768
234
235
  runtimepy/net/tcp/telnet/codes.py,sha256=1-yyRe-Kz_W7d6B0P3iT1AaSNR3_Twmn-MUjKCJJknY,3518
@@ -246,7 +247,7 @@ runtimepy/net/udp/tftp/io.py,sha256=w6cnUt-T-Ma6Vg8BWoRbsNnIWUv0HTY4am6bcLWxNJs,
246
247
  runtimepy/net/websocket/__init__.py,sha256=YjSmoxiigmsI_hcQw6nueX7bxhrRGerEERnPvgLVEVA,313
247
248
  runtimepy/net/websocket/connection.py,sha256=BMR58bLpHuulCdbLGnmMdFJOF53wVxYcUe52WbdzKEM,9025
248
249
  runtimepy/noise/__init__.py,sha256=EJM7h3t_z74wwrn6FAFQwYE2yUcOZQ1K1IQqOb8Z0AI,384
249
- runtimepy/primitives/__init__.py,sha256=uyihG0zIzarH99iskrL97VUd44m6AqNoLZPF5cC7q9A,2569
250
+ runtimepy/primitives/__init__.py,sha256=g5IK6e_UZuGhj9QNYoCn5TbhNatWX0nEYUs-JzsOizQ,2618
250
251
  runtimepy/primitives/base.py,sha256=BaGPUTeVMnLnTPcpjqnS2lzPN74Pe5C0XaQdgrTfW7A,9185
251
252
  runtimepy/primitives/bool.py,sha256=lATPgb1e62rjLn5XlJX8lP3tVYR3DlxV8RT9HpOMdT0,1640
252
253
  runtimepy/primitives/byte_order.py,sha256=J2Pg3gXg8Lmu_uU2mduNJt3mnP_enwDI4Y-X8kWAUP0,1456
@@ -301,9 +302,9 @@ runtimepy/tui/task.py,sha256=nUZo9fuOC-k1Wpqdzkv9v1tQirCI28fZVgcC13Ijvus,1093
301
302
  runtimepy/tui/channels/__init__.py,sha256=evDaiIn-YS9uGhdo8ZGtP9VK1ek6sr_P1nJ9JuSET0o,4536
302
303
  runtimepy/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
303
304
  runtimepy/ui/controls.py,sha256=yvT7h3thbYaitsakcIAJ90EwKzJ4b-jnc6p3UuVf_XE,1241
304
- runtimepy-5.13.2.dist-info/licenses/LICENSE,sha256=yKBRwbO-cOPBrlpsZmJkkSa33DfY31aE8t7lZ0DwlUo,1071
305
- runtimepy-5.13.2.dist-info/METADATA,sha256=_GkgF9xsxR9UGBpUSeJ0of9xjc783dgtAMH8GltwoRs,9269
306
- runtimepy-5.13.2.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
307
- runtimepy-5.13.2.dist-info/entry_points.txt,sha256=-btVBkYv7ybcopqZ_pRky-bEzu3vhbaG3W3Z7ERBiFE,51
308
- runtimepy-5.13.2.dist-info/top_level.txt,sha256=0jPmh6yqHyyJJDwEID-LpQly-9kQ3WRMjH7Lix8peLg,10
309
- runtimepy-5.13.2.dist-info/RECORD,,
305
+ runtimepy-5.14.0.dist-info/licenses/LICENSE,sha256=yKBRwbO-cOPBrlpsZmJkkSa33DfY31aE8t7lZ0DwlUo,1071
306
+ runtimepy-5.14.0.dist-info/METADATA,sha256=7g1Ho2eto4w_nLS1GXjqyuHOlAZYXvFHXeYoOq8s194,9269
307
+ runtimepy-5.14.0.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
308
+ runtimepy-5.14.0.dist-info/entry_points.txt,sha256=-btVBkYv7ybcopqZ_pRky-bEzu3vhbaG3W3Z7ERBiFE,51
309
+ runtimepy-5.14.0.dist-info/top_level.txt,sha256=0jPmh6yqHyyJJDwEID-LpQly-9kQ3WRMjH7Lix8peLg,10
310
+ runtimepy-5.14.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.1)
2
+ Generator: setuptools (80.4.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5