runtimepy 5.12.1__py3-none-any.whl → 5.13.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.1
4
- # hash=ba0d30a48267eea48f57090cd3a871b2
4
+ # hash=35e5555173fdfd064e5047172de9b6a4
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.12.1"
13
+ VERSION = "5.13.0"
14
14
 
15
15
  # runtimepy-specific content.
16
16
  METRICS_NAME = "metrics"
@@ -48,7 +48,6 @@ def get_kwargs(args: _Namespace, **kwargs) -> dict[str, Any]:
48
48
  new_kwargs: dict[str, Any] = {**kwargs}
49
49
 
50
50
  # Pass additional arguments through.
51
- print(args)
52
51
  for opt in PASSTHROUGH:
53
52
  value = getattr(args, opt, None)
54
53
  if value is not None:
@@ -6,8 +6,12 @@ A module implementing HTTP-response interfaces.
6
6
  import http
7
7
  from io import StringIO
8
8
  import logging
9
+ from pathlib import Path
10
+ from typing import AsyncIterator, cast
9
11
 
10
12
  # third-party
13
+ import aiofiles
14
+ import aiofiles.os
11
15
  from vcorelib.logging import LoggerType
12
16
 
13
17
  # internal
@@ -83,3 +87,31 @@ class ResponseHeader(HeadersMixin):
83
87
  self.status_line,
84
88
  self.headers,
85
89
  )
90
+
91
+
92
+ class AsyncResponse:
93
+ """
94
+ A class facilitating asynchronous server responses for e.g.
95
+ file-system files.
96
+ """
97
+
98
+ def __init__(self, path: Path, chunk_size: int = 1024) -> None:
99
+ """Initialize this instance."""
100
+
101
+ self.path = path
102
+ self.chunk_size = chunk_size
103
+
104
+ async def size(self) -> int:
105
+ """Get this response's size."""
106
+ return cast(int, await aiofiles.os.path.getsize(self.path))
107
+
108
+ async def process(self) -> AsyncIterator[bytes]:
109
+ """Yield chunks to write asynchronously."""
110
+
111
+ async with aiofiles.open(self.path, mode="rb") as path_fd:
112
+ while True:
113
+ data = await path_fd.read(self.chunk_size)
114
+ if data:
115
+ yield data
116
+ else:
117
+ break
@@ -21,11 +21,11 @@ from runtimepy.channel.environment.command import GLOBAL
21
21
  from runtimepy.net.html import full_markdown_page
22
22
  from runtimepy.net.http.header import RequestHeader
23
23
  from runtimepy.net.http.request_target import PathMaybeQuery
24
- from runtimepy.net.http.response import ResponseHeader
24
+ from runtimepy.net.http.response import AsyncResponse, ResponseHeader
25
25
  from runtimepy.net.server.html import HtmlApp, HtmlApps, get_html, html_handler
26
26
  from runtimepy.net.server.json import encode_json, json_handler
27
27
  from runtimepy.net.server.markdown import markdown_for_dir
28
- from runtimepy.net.tcp.http import HttpConnection
28
+ from runtimepy.net.tcp.http import HttpConnection, HttpResult
29
29
  from runtimepy.util import normalize_root, path_has_part, read_binary
30
30
 
31
31
  MIMETYPES_INIT = False
@@ -173,10 +173,10 @@ class RuntimepyServerConnection(HttpConnection):
173
173
 
174
174
  async def try_file(
175
175
  self, path: PathMaybeQuery, response: ResponseHeader
176
- ) -> Optional[bytes]:
176
+ ) -> HttpResult:
177
177
  """Try serving this path as a file directly from the file-system."""
178
178
 
179
- result = None
179
+ result: HttpResult = None
180
180
 
181
181
  # Keep track of directories encountered.
182
182
  directories: list[Path] = []
@@ -214,7 +214,7 @@ class RuntimepyServerConnection(HttpConnection):
214
214
  self.logger.info("Serving '%s' (MIME: %s)", candidate, mime)
215
215
 
216
216
  # Return the file data.
217
- result = await read_binary(candidate)
217
+ result = AsyncResponse(candidate)
218
218
  break
219
219
 
220
220
  # Handle a directory as a last resort.
@@ -259,7 +259,7 @@ class RuntimepyServerConnection(HttpConnection):
259
259
  response: ResponseHeader,
260
260
  request: RequestHeader,
261
261
  request_data: Optional[bytes],
262
- ) -> Optional[bytes]:
262
+ ) -> HttpResult:
263
263
  """Handle POST requests."""
264
264
 
265
265
  request.log(self.logger, False, level=logging.INFO)
@@ -281,7 +281,7 @@ class RuntimepyServerConnection(HttpConnection):
281
281
  response: ResponseHeader,
282
282
  request: RequestHeader,
283
283
  request_data: Optional[bytes],
284
- ) -> Optional[bytes]:
284
+ ) -> HttpResult:
285
285
  """Handle GET requests."""
286
286
 
287
287
  request.log(self.logger, False, level=logging.INFO)
@@ -16,22 +16,24 @@ from vcorelib import DEFAULT_ENCODING
16
16
  from runtimepy import PKG_NAME, VERSION
17
17
  from runtimepy.net.http import HttpMessageProcessor
18
18
  from runtimepy.net.http.header import RequestHeader
19
- from runtimepy.net.http.response import ResponseHeader
19
+ from runtimepy.net.http.response import AsyncResponse, ResponseHeader
20
20
  from runtimepy.net.tcp.connection import TcpConnection as _TcpConnection
21
21
 
22
+ HttpResult = Optional[bytes | AsyncResponse]
23
+
22
24
  #
23
25
  # async def handler(
24
26
  # response: ResponseHeader,
25
27
  # request: RequestHeader,
26
28
  # request_data: Optional[bytes],
27
- # ) -> Optional[bytes]:
29
+ # ) -> HttpResult:
28
30
  # """Sample handler."""
29
31
  #
30
32
  HttpRequestHandler = Callable[
31
33
  [ResponseHeader, RequestHeader, Optional[bytes]],
32
- Awaitable[Optional[bytes]],
34
+ Awaitable[HttpResult],
33
35
  ]
34
- HttpResponse = Tuple[ResponseHeader, Optional[bytes]]
36
+ HttpResponse = Tuple[ResponseHeader, HttpResult]
35
37
 
36
38
  HttpRequestHandlers = dict[http.HTTPMethod, HttpRequestHandler]
37
39
 
@@ -89,7 +91,7 @@ class HttpConnection(_TcpConnection):
89
91
  response: ResponseHeader,
90
92
  request: RequestHeader,
91
93
  request_data: Optional[bytes],
92
- ) -> Optional[bytes]:
94
+ ) -> HttpResult:
93
95
  """Sample handler."""
94
96
 
95
97
  async def post_handler(
@@ -97,7 +99,7 @@ class HttpConnection(_TcpConnection):
97
99
  response: ResponseHeader,
98
100
  request: RequestHeader,
99
101
  request_data: Optional[bytes],
100
- ) -> Optional[bytes]:
102
+ ) -> HttpResult:
101
103
  """Sample handler."""
102
104
 
103
105
  async def _process_request(
@@ -105,7 +107,7 @@ class HttpConnection(_TcpConnection):
105
107
  response: ResponseHeader,
106
108
  request_header: RequestHeader,
107
109
  request_data: Optional[bytes] = None,
108
- ) -> Optional[bytes]:
110
+ ) -> HttpResult:
109
111
  """Process an individual request."""
110
112
 
111
113
  result = None
@@ -137,7 +139,7 @@ class HttpConnection(_TcpConnection):
137
139
  # Set boilerplate header data.
138
140
  request["user-agent"] = self.identity
139
141
 
140
- self._send(request, data)
142
+ await self._send(request, data)
141
143
  self.expecting_response = True
142
144
  result = await self.responses.get()
143
145
  self.expecting_response = False
@@ -153,21 +155,29 @@ class HttpConnection(_TcpConnection):
153
155
  """
154
156
  return to_json(await self.request(request, data))
155
157
 
156
- def _send(
158
+ async def _send(
157
159
  self,
158
160
  header: Union[ResponseHeader, RequestHeader],
159
- data: Optional[bytes] = None,
161
+ data: HttpResult = None,
160
162
  ) -> None:
161
163
  """Send a request or response to a request."""
162
164
 
163
165
  # Set content length.
164
166
  header["content-length"] = "0"
165
- if data:
167
+
168
+ if isinstance(data, AsyncResponse):
169
+ header["content-length"] = str(await data.size())
170
+ elif data is not None:
166
171
  header["content-length"] = str(len(data))
167
172
 
168
173
  self.send_binary(bytes(header))
169
- if data:
170
- self.send_binary(data)
174
+
175
+ if data is not None:
176
+ if isinstance(data, AsyncResponse):
177
+ async for chunk in data.process():
178
+ self.send_binary(chunk)
179
+ else:
180
+ self.send_binary(data)
171
181
 
172
182
  header.log(self.logger, True)
173
183
 
@@ -183,7 +193,7 @@ class HttpConnection(_TcpConnection):
183
193
  if not self.expecting_response:
184
194
  # Process request.
185
195
  response = ResponseHeader()
186
- self._send(
196
+ await self._send(
187
197
  response,
188
198
  await self._process_request(
189
199
  response, cast(RequestHeader, header), payload
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtimepy
3
- Version: 5.12.1
3
+ Version: 5.13.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
@@ -18,10 +18,10 @@ Requires-Python: >=3.12
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: svgen>=0.7.4
21
- Requires-Dist: vcorelib>=3.5.1
22
- Requires-Dist: psutil
23
21
  Requires-Dist: aiofiles
22
+ Requires-Dist: psutil
24
23
  Requires-Dist: websockets
24
+ Requires-Dist: vcorelib>=3.5.1
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.1
54
- hash=6b0b773eaaa04369914e4480e7e90086
54
+ hash=4f0e82dc45d3ea9c4ccf5b262413c206
55
55
  =====================================
56
56
  -->
57
57
 
58
- # runtimepy ([5.12.1](https://pypi.org/project/runtimepy/))
58
+ # runtimepy ([5.13.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)
@@ -65,6 +65,8 @@ Dynamic: requires-python
65
65
 
66
66
  *A framework for implementing Python services.*
67
67
 
68
+ Consider [sponsoring development](https://github.com/sponsors/libre-embedded).
69
+
68
70
  ([interface documentation](https://libre-embedded.github.io/python/runtimepy))
69
71
 
70
72
  ## Python Version Support
@@ -1,4 +1,4 @@
1
- runtimepy/__init__.py,sha256=aVwCNKJ1UuAVGt2ygKy9jh9rKssg7ylq5gWjnll6ZCo,391
1
+ runtimepy/__init__.py,sha256=HxjInsMBuhZ8HVgWbkL2C4cBfavqCLORI0hiaUc4Lq0,391
2
2
  runtimepy/__main__.py,sha256=3IFTvHw-vqPIFy6ZZjzl00oDwFLzcDmV5WHo3ohPKbc,332
3
3
  runtimepy/app.py,sha256=xhqZOvBcBCawUR2Qd5oyvrmNdOJhdChgddb0GiJOnPc,970
4
4
  runtimepy/dev_requirements.txt,sha256=VZhW6bJ5YbwaoN4d_XxZFuN5BbDLaG7ngKrGnugVPRw,245
@@ -33,7 +33,7 @@ runtimepy/commands/all.py,sha256=nANQ5v5PlIhY8TZejcckGoZmqjOr7tYA7LstordQUDk,161
33
33
  runtimepy/commands/arbiter.py,sha256=CtTMRYpqCAN3vWHkkr9jqWpoF7JGNXafKIBFmkarAfc,1567
34
34
  runtimepy/commands/common.py,sha256=NvZdeIFBHAF52c1n7vqD59DW6ywc-rG5iC5MpuhGf-c,2449
35
35
  runtimepy/commands/mtu.py,sha256=LFFjTU4SsuV3j7Mhx_WuKa5lfdfMm70zJvDWToVrP7E,1357
36
- runtimepy/commands/server.py,sha256=G1q6bgq0HpsKvDFcPyAC--f3-OuZqtnR4FK7b36Rqv8,4671
36
+ runtimepy/commands/server.py,sha256=q49WlQQWYBcHrKQo7hCc3Y6hNbG2DNLMXIlLg8Mc8lA,4655
37
37
  runtimepy/commands/task.py,sha256=6xRVlRwpEZVhrcY18sQcfdWEOxeQZLeOF-6UrUURtO4,1435
38
38
  runtimepy/commands/tftp.py,sha256=djFQzYDxy2jvUseHJ4fDR3CowPxQ-Tu0IJz1SwapXX0,2361
39
39
  runtimepy/commands/tui.py,sha256=9hWA3_YATibUUDTVQr7UnKzPTDVJ7WxWKTYYQpLoyrE,1869
@@ -190,10 +190,10 @@ runtimepy/net/http/__init__.py,sha256=4TjFp_ajAVcOEvwtjlF6mG-9EbEePqFZht-QpWIKVB
190
190
  runtimepy/net/http/common.py,sha256=vpoO6XwRmrZmTkCu9bkI0HnyaD8MWTpV7ADesCNrfRE,2237
191
191
  runtimepy/net/http/header.py,sha256=AECSdvhBA9_5Pg3UdwMzsmBpcqgsiPj41xnIGPm5g5E,2296
192
192
  runtimepy/net/http/request_target.py,sha256=EE1aI5VSARw1h93jyZvP56ir5O5fjd6orYK-1FM4JNk,1550
193
- runtimepy/net/http/response.py,sha256=Sup8W_A0ADNzR5olKrQsVNhsQXUwPOD-eJLlLOgYlAY,2316
193
+ runtimepy/net/http/response.py,sha256=y33KLUJNE7zx6biUMwTkUfQ1bXiKV6pjrcxuh8U3WCE,3216
194
194
  runtimepy/net/http/state.py,sha256=qCMN8aWfCRfU9XP-cIhSOo2RqfljTjbQRCflfcy2bfY,1626
195
195
  runtimepy/net/http/version.py,sha256=mp6rgIM7-VUVKLCA0Uw96CmBkL0ET860lDVVEewpZ7w,1098
196
- runtimepy/net/server/__init__.py,sha256=RojwvQgqc0rTl36rXDSiM56tVXtD15T5OkpuJanlNjY,10438
196
+ runtimepy/net/server/__init__.py,sha256=R6xV5wUqrCwB0pa0FMjcZ-txkT87H_B9AgWq0RrJQsU,10458
197
197
  runtimepy/net/server/html.py,sha256=ufg0PQF_iUE7PT0n3Pn3jTcun7mspZUI6_ooblcNnvI,1217
198
198
  runtimepy/net/server/json.py,sha256=a7vM5yfq2er4DexzFqEMnxoMGDeuywKkVH4-uNJBAik,2522
199
199
  runtimepy/net/server/markdown.py,sha256=DFjGbvIST4HXRhtTTvXVQ9ZAwrIfVzPlcU1dW8JYya8,1381
@@ -227,7 +227,7 @@ runtimepy/net/tcp/__init__.py,sha256=OOWohegpoioSTf8M7uDf-4EV1IDungz7-U19L_2yW4I
227
227
  runtimepy/net/tcp/connection.py,sha256=sYWj2aDiAHQf70zfRJM24cHraFd_TuzTD4fRWeSQZXE,8811
228
228
  runtimepy/net/tcp/create.py,sha256=zZsRs5KYpO3bNGh-DwEOEzjUDE4ixj-UBHYgZ0GvC7c,2013
229
229
  runtimepy/net/tcp/protocol.py,sha256=vEnIX3gUX2nrw9ofT_e4KYU4VY2k4WP0WuOi4eE_OOQ,1444
230
- runtimepy/net/tcp/http/__init__.py,sha256=BY9ra0s7kHkoUQKIRHsnk3_WDWP7Xt-FxQ90K14nwkc,5737
230
+ runtimepy/net/tcp/http/__init__.py,sha256=SPvrZvIM1poE_jgCNY6gjgV2bXfMmPH_0zdx8x4JSVI,6080
231
231
  runtimepy/net/tcp/scpi/__init__.py,sha256=aWCWQfdeyfoU9bpOnOtyIQbT1swl4ergXLFn5kXAH28,2105
232
232
  runtimepy/net/tcp/telnet/__init__.py,sha256=96eJFb301I3H2ivDtGMQtDDw09Xm5NRvM9VEC-wjt8c,4768
233
233
  runtimepy/net/tcp/telnet/codes.py,sha256=1-yyRe-Kz_W7d6B0P3iT1AaSNR3_Twmn-MUjKCJJknY,3518
@@ -300,9 +300,9 @@ runtimepy/tui/task.py,sha256=nUZo9fuOC-k1Wpqdzkv9v1tQirCI28fZVgcC13Ijvus,1093
300
300
  runtimepy/tui/channels/__init__.py,sha256=evDaiIn-YS9uGhdo8ZGtP9VK1ek6sr_P1nJ9JuSET0o,4536
301
301
  runtimepy/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
302
302
  runtimepy/ui/controls.py,sha256=yvT7h3thbYaitsakcIAJ90EwKzJ4b-jnc6p3UuVf_XE,1241
303
- runtimepy-5.12.1.dist-info/licenses/LICENSE,sha256=yKBRwbO-cOPBrlpsZmJkkSa33DfY31aE8t7lZ0DwlUo,1071
304
- runtimepy-5.12.1.dist-info/METADATA,sha256=rxNyFXioax6ETUP7icHD1ZopQsJCx8u-OifIMcBjFMw,9188
305
- runtimepy-5.12.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
306
- runtimepy-5.12.1.dist-info/entry_points.txt,sha256=-btVBkYv7ybcopqZ_pRky-bEzu3vhbaG3W3Z7ERBiFE,51
307
- runtimepy-5.12.1.dist-info/top_level.txt,sha256=0jPmh6yqHyyJJDwEID-LpQly-9kQ3WRMjH7Lix8peLg,10
308
- runtimepy-5.12.1.dist-info/RECORD,,
303
+ runtimepy-5.13.0.dist-info/licenses/LICENSE,sha256=yKBRwbO-cOPBrlpsZmJkkSa33DfY31aE8t7lZ0DwlUo,1071
304
+ runtimepy-5.13.0.dist-info/METADATA,sha256=TEucfXpt7dgg0L_P1F-WItb0l7MJuXLyuejhWBc1Uv0,9268
305
+ runtimepy-5.13.0.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
306
+ runtimepy-5.13.0.dist-info/entry_points.txt,sha256=-btVBkYv7ybcopqZ_pRky-bEzu3vhbaG3W3Z7ERBiFE,51
307
+ runtimepy-5.13.0.dist-info/top_level.txt,sha256=0jPmh6yqHyyJJDwEID-LpQly-9kQ3WRMjH7Lix8peLg,10
308
+ runtimepy-5.13.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.0)
2
+ Generator: setuptools (80.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5