runtimepy 5.15.6__py3-none-any.whl → 5.15.8__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=08c1dda8bd5fefe13af6a8eb49063498
4
+ # hash=be3f79364e840f1950bc121205810f02
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.15.6"
13
+ VERSION = "5.15.8"
14
14
 
15
15
  # runtimepy-specific content.
16
16
  METRICS_NAME = "metrics"
@@ -82,3 +82,7 @@ body > :first-child {
82
82
  .channel-filter-min-width {
83
83
  min-width: 5.5em;
84
84
  }
85
+
86
+ .media-preview {
87
+ max-width: 70vw;
88
+ }
@@ -3,11 +3,12 @@ A module implementing HTTP-response interfaces.
3
3
  """
4
4
 
5
5
  # built-in
6
+ from abc import ABC, abstractmethod
6
7
  import http
7
8
  from io import StringIO
8
9
  import logging
9
10
  from pathlib import Path
10
- from typing import AsyncIterator, cast
11
+ from typing import AsyncIterator, Optional, cast
11
12
 
12
13
  # third-party
13
14
  import aiofiles
@@ -96,19 +97,32 @@ class ResponseHeader(HeadersMixin):
96
97
  self["Cache-Control"] = value
97
98
 
98
99
 
99
- class AsyncResponse:
100
+ class AsyncResponse(ABC):
101
+ """Interface for asynchronous responses."""
102
+
103
+ @abstractmethod
104
+ async def size(self) -> Optional[int]:
105
+ """Get this response's size."""
106
+
107
+ @abstractmethod
108
+ async def process(self) -> AsyncIterator[bytes]:
109
+ """Yield chunks to write asynchronously."""
110
+ yield bytes() # pragma: nocover
111
+
112
+
113
+ class AsyncFile(AsyncResponse):
100
114
  """
101
115
  A class facilitating asynchronous server responses for e.g.
102
116
  file-system files.
103
117
  """
104
118
 
105
- def __init__(self, path: Path, chunk_size: int = 1024) -> None:
119
+ def __init__(self, path: Path, chunk_size: int = 4096) -> None:
106
120
  """Initialize this instance."""
107
121
 
108
122
  self.path = path
109
123
  self.chunk_size = chunk_size
110
124
 
111
- async def size(self) -> int:
125
+ async def size(self) -> Optional[int]:
112
126
  """Get this response's size."""
113
127
  return cast(int, await aiofiles.os.path.getsize(self.path))
114
128
 
@@ -8,7 +8,7 @@ from io import StringIO
8
8
  import logging
9
9
  import mimetypes
10
10
  from pathlib import Path
11
- from typing import Any, Optional, TextIO, Union
11
+ from typing import Any, Awaitable, Callable, Optional, TextIO, Union
12
12
  from urllib.parse import urlencode
13
13
 
14
14
  # third-party
@@ -22,7 +22,7 @@ from runtimepy.channel.environment.command import GLOBAL, global_command
22
22
  from runtimepy.net.html import full_markdown_page
23
23
  from runtimepy.net.http.header import RequestHeader
24
24
  from runtimepy.net.http.request_target import PathMaybeQuery
25
- from runtimepy.net.http.response import AsyncResponse, ResponseHeader
25
+ from runtimepy.net.http.response import AsyncFile, ResponseHeader
26
26
  from runtimepy.net.server.html import HtmlApp, HtmlApps, get_html, html_handler
27
27
  from runtimepy.net.server.json import encode_json, json_handler
28
28
  from runtimepy.net.server.markdown import DIR_FILE, markdown_for_dir
@@ -41,11 +41,17 @@ def package_data_dir() -> Path:
41
41
  return result.parent
42
42
 
43
43
 
44
+ CustomAsync = Callable[
45
+ [ResponseHeader, RequestHeader, Optional[bytearray]], Awaitable[HttpResult]
46
+ ]
47
+
48
+
44
49
  class RuntimepyServerConnection(HttpConnection):
45
50
  """A class implementing a server-connection interface for this package."""
46
51
 
47
52
  # Can register application methods to URL paths.
48
53
  apps: HtmlApps = {"/mux.html": mux_app}
54
+ custom: dict[str, CustomAsync] = {}
49
55
  default_app: Optional[HtmlApp] = None
50
56
 
51
57
  # Can load additional data into this dictionary for easy HTTP access.
@@ -245,7 +251,7 @@ class RuntimepyServerConnection(HttpConnection):
245
251
  response.static_resource()
246
252
 
247
253
  # Return the file data.
248
- result = AsyncResponse(candidate)
254
+ result = AsyncFile(candidate)
249
255
  break
250
256
 
251
257
  # Handle a directory as a last resort.
@@ -335,6 +341,12 @@ class RuntimepyServerConnection(HttpConnection):
335
341
  response.static_resource()
336
342
  return self.favicon_data
337
343
 
344
+ # Check for a custom handler.
345
+ if path in self.custom:
346
+ return await self.custom[path](
347
+ response, request, request_data
348
+ )
349
+
338
350
  # Try serving a file and handling redirects.
339
351
  for handler in [self.try_redirect, self.try_file]:
340
352
  result = await handler(
@@ -5,6 +5,7 @@ A module implementing web server markdown interfaces.
5
5
  # built-in
6
6
  from io import StringIO
7
7
  import mimetypes
8
+ from os import stat_result
8
9
  from pathlib import Path
9
10
  from typing import Iterable, cast
10
11
 
@@ -17,19 +18,46 @@ from vcorelib.paths import stats as _stats
17
18
 
18
19
  LOGO_MARKDOWN = "[![logo](/static/png/chip-circle-bootstrap/128x128.png)](/)"
19
20
  DIR_FILE = "dir.html"
21
+ AUTOPLAY_PREVIEW_SIZE = 100 * (1024 * 1024) # 100 MiB
20
22
 
21
23
 
22
- def file_preview(path: Path, link: Path) -> str:
24
+ def file_preview(path: Path, link: Path, stats: stat_result) -> str:
23
25
  """Get possible preview text for a file."""
24
26
 
25
27
  preview = ""
26
28
 
27
- if path.is_file():
28
- mime, _ = mimetypes.guess_type(path, strict=False)
29
- if mime and mime.startswith("image"):
30
- preview = div(tag="img", src=f"/{link}", alt=str(link)).encode_str(
31
- newlines=False
32
- )
29
+ if not path.is_file():
30
+ return preview
31
+
32
+ mime, _ = mimetypes.guess_type(path, strict=False)
33
+ if mime:
34
+ # Image previews.
35
+ if mime.startswith("image"):
36
+ preview = div(
37
+ tag="img",
38
+ src=f"/{link}",
39
+ alt=str(link),
40
+ class_str="media-preview",
41
+ ).encode_str(newlines=False)
42
+
43
+ # Video previews.
44
+ elif mime.startswith("video"):
45
+ elem = div(tag="video", class_str="media-preview")
46
+ elem.booleans.add("loop")
47
+ elem.booleans.add("controls")
48
+
49
+ if stats.st_size < AUTOPLAY_PREVIEW_SIZE:
50
+ elem.booleans.add("autoplay")
51
+
52
+ div(parent=elem, tag="source", src=f"/{link}", type=mime)
53
+
54
+ preview = elem.encode_str(newlines=False)
55
+
56
+ # Audio previews.
57
+ elif mime.startswith("audio"):
58
+ elem = div(tag="audio", src=f"/{link}")
59
+ elem.booleans.add("controls")
60
+ preview = elem.encode_str(newlines=False)
33
61
 
34
62
  return preview
35
63
 
@@ -65,7 +93,8 @@ def write_markdown_dir(
65
93
  size_str = byte_count_str(stats.st_size) if item.is_file() else ""
66
94
 
67
95
  writer.write(
68
- f"| [{name}](/{curr}) | {size_str} | {file_preview(item, curr)} |"
96
+ f"| [{name}](/{curr}) | {size_str} | "
97
+ f"{file_preview(item, curr, stats)} |"
69
98
  )
70
99
 
71
100
  writer.empty()
@@ -173,13 +173,13 @@ class HttpConnection(_TcpConnection):
173
173
  ) -> None:
174
174
  """Send a request or response to a request."""
175
175
 
176
- # Set content length.
177
- header["content-length"] = "0"
178
-
176
+ size = None
179
177
  if isinstance(data, AsyncResponse):
180
- header["content-length"] = str(await data.size())
178
+ size = await data.size()
181
179
  elif data is not None:
182
- header["content-length"] = str(len(data))
180
+ size = len(data)
181
+ if size is not None:
182
+ header["content-length"] = str(size)
183
183
 
184
184
  self.send_binary(bytes(header))
185
185
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtimepy
3
- Version: 5.15.6
3
+ Version: 5.15.8
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
- Requires-Dist: svgen>=0.8.0
22
- Requires-Dist: websockets
23
20
  Requires-Dist: psutil
24
21
  Requires-Dist: vcorelib>=3.6.4
22
+ Requires-Dist: websockets
23
+ Requires-Dist: svgen>=0.8.0
24
+ Requires-Dist: aiofiles
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=0f8ffdbd46445346cb834d066f58e765
54
+ hash=c354cb6439439285d244ad2bdcc32cc8
55
55
  =====================================
56
56
  -->
57
57
 
58
- # runtimepy ([5.15.6](https://pypi.org/project/runtimepy/))
58
+ # runtimepy ([5.15.8](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,4 +1,4 @@
1
- runtimepy/__init__.py,sha256=qAS3P1hKgQCdNqiMTEqhiSnIM9BIwdZqBvOQfrZCBC4,391
1
+ runtimepy/__init__.py,sha256=uc9YAPW-YaZX4q8Y53kePOKLHa19DXc_OwE9dc7py2M,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
@@ -55,7 +55,7 @@ runtimepy/data/server_dev.yaml,sha256=nQsPh7LuQig3pzHfdg_aD3yOUiCj1sKKfI-WwW3hXm
55
55
  runtimepy/data/tftp_server.yaml,sha256=-bFOWJSagI-fEQQcT8k7eDMJVfSPm2XAxLVG3dqUTa4,204
56
56
  runtimepy/data/css/bootstrap_extra.css,sha256=0NiTnZWnIYf04n9NHVe-nVSOLwePJ6X-3uCgUgAEKPI,3101
57
57
  runtimepy/data/css/font.css,sha256=Pe82E66rNi-cwlQ-_1GHAuhPGu5L4x5KqgV0dbDe51w,977
58
- runtimepy/data/css/main.css,sha256=B3cBrwz6Leox1tYHTzJ7dW7Le0gf5_qZDCNlRsu-0aY,1316
58
+ runtimepy/data/css/main.css,sha256=ohZ98-ffw6U1h2Azj8ToYiXFurQo1rH9r6kFQZkrtQ0,1355
59
59
  runtimepy/data/js/DataConnection.js,sha256=DnX8FMehjJXqmI62UMYXSvl_XdfQMzq3XUDFbLu2GgI,98
60
60
  runtimepy/data/js/JsonConnection.js,sha256=rclZrbmWc_zSs6I_JhOgxnVPFIyPMo5WdjAe8alyZ3o,2729
61
61
  runtimepy/data/js/audio.js,sha256=bLkBqbeHMiGGidfL3iXjmVoF9seK-ZeZ3kwgOrcpgk4,1092
@@ -199,13 +199,13 @@ runtimepy/net/http/__init__.py,sha256=cMKNPyYZpdT03OR-HNfwzZbaKGchzlGIrK712daRK4
199
199
  runtimepy/net/http/common.py,sha256=vpoO6XwRmrZmTkCu9bkI0HnyaD8MWTpV7ADesCNrfRE,2237
200
200
  runtimepy/net/http/header.py,sha256=AECSdvhBA9_5Pg3UdwMzsmBpcqgsiPj41xnIGPm5g5E,2296
201
201
  runtimepy/net/http/request_target.py,sha256=EfcOozUeXqOuQaMXH9cErfJqUkG0A5v9HEK4tCorSf4,1543
202
- runtimepy/net/http/response.py,sha256=fD0R_BUgmNwdwKQtXvYfTYM7DyJlwsGmlNVi5HkCOhU,3409
202
+ runtimepy/net/http/response.py,sha256=rLllTaaFn3-ABMhq3jM_6-kqq8ym5VJGVWNwmBr3kAI,3823
203
203
  runtimepy/net/http/state.py,sha256=qCMN8aWfCRfU9XP-cIhSOo2RqfljTjbQRCflfcy2bfY,1626
204
204
  runtimepy/net/http/version.py,sha256=mp6rgIM7-VUVKLCA0Uw96CmBkL0ET860lDVVEewpZ7w,1098
205
- runtimepy/net/server/__init__.py,sha256=ppfMgymYGRz13MvHvwXyy9WAos2wWgaZDf1TW8D1ILU,11813
205
+ runtimepy/net/server/__init__.py,sha256=NHs8cXTRTEreVhk5QW9dMTW-pyUCd3NfuATm1a-5V6Y,12191
206
206
  runtimepy/net/server/html.py,sha256=OB2K37kA7MHtcKqp0pZE1q_XgiyvfooigzS0-OQFCCM,1900
207
207
  runtimepy/net/server/json.py,sha256=AaMPw-G-7xX67mf1LvQAipNdwrnmbLDunVlkWf6iOz0,2526
208
- runtimepy/net/server/markdown.py,sha256=KuPKYh7nj5oy70Ff8LRAXwr_Ga4r8_3KksvDfPfFqYM,2690
208
+ runtimepy/net/server/markdown.py,sha256=kKKSGF_HwH8ADnNm5fNnWq4XXh7Td9Iu_QUj_u-qmts,3595
209
209
  runtimepy/net/server/mux.py,sha256=ejET7NpJzzJjkVwR8laSuZYR79DOT3XazmAIm6rENSE,598
210
210
  runtimepy/net/server/app/__init__.py,sha256=0eCcqYvmby_wQTGqAiGbGM5LCxKM2qpjuIT0gLHsoDU,3157
211
211
  runtimepy/net/server/app/base.py,sha256=46aOqZwRss_nh_WfEH1cMJ9GUVoLJjERd7cTRFu6mXE,1878
@@ -237,7 +237,7 @@ runtimepy/net/tcp/__init__.py,sha256=OOWohegpoioSTf8M7uDf-4EV1IDungz7-U19L_2yW4I
237
237
  runtimepy/net/tcp/connection.py,sha256=FL9SlODVAehECYgF9eKk8sbDMmTMBGlsGJlGaMncQh4,8793
238
238
  runtimepy/net/tcp/create.py,sha256=zZsRs5KYpO3bNGh-DwEOEzjUDE4ixj-UBHYgZ0GvC7c,2013
239
239
  runtimepy/net/tcp/protocol.py,sha256=vT4HOH-WLrM0j4kUpQA_ZZOwA-kJPMP9M40ptuL84_0,1412
240
- runtimepy/net/tcp/http/__init__.py,sha256=0nEB6KFqV9GxMwFKqKIswA0wihHfbLP5UD-r5ulgTj0,6391
240
+ runtimepy/net/tcp/http/__init__.py,sha256=7Pnge4d50hG6QjcTXb1lZW1Xcver-vLVubBhj-WUduQ,6369
241
241
  runtimepy/net/tcp/scpi/__init__.py,sha256=FcNouVQh0A27-x16lzTbt7vZfWs4bRU9uZqbeVjDfiU,2173
242
242
  runtimepy/net/tcp/telnet/__init__.py,sha256=2pGaG-IlZA29BM6QAvI4q9w484fgvypX4fOIioDem94,4814
243
243
  runtimepy/net/tcp/telnet/codes.py,sha256=1-yyRe-Kz_W7d6B0P3iT1AaSNR3_Twmn-MUjKCJJknY,3518
@@ -311,9 +311,9 @@ runtimepy/tui/channels/__init__.py,sha256=evDaiIn-YS9uGhdo8ZGtP9VK1ek6sr_P1nJ9Ju
311
311
  runtimepy/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
312
312
  runtimepy/ui/button.py,sha256=8MXnhqmVvANwbbTA8gu3b8N4IIvRYicnwxFQD6SZy3U,2045
313
313
  runtimepy/ui/controls.py,sha256=L55Af-4vGq6ZHewdoA7C_mAYq35WXl8NzOdcsmQIo7M,1868
314
- runtimepy-5.15.6.dist-info/licenses/LICENSE,sha256=yKBRwbO-cOPBrlpsZmJkkSa33DfY31aE8t7lZ0DwlUo,1071
315
- runtimepy-5.15.6.dist-info/METADATA,sha256=MoIujpmp5ifEdr-DxKZrNXyHuFSUoeS6iX0iIyqdW2w,9268
316
- runtimepy-5.15.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
317
- runtimepy-5.15.6.dist-info/entry_points.txt,sha256=-btVBkYv7ybcopqZ_pRky-bEzu3vhbaG3W3Z7ERBiFE,51
318
- runtimepy-5.15.6.dist-info/top_level.txt,sha256=0jPmh6yqHyyJJDwEID-LpQly-9kQ3WRMjH7Lix8peLg,10
319
- runtimepy-5.15.6.dist-info/RECORD,,
314
+ runtimepy-5.15.8.dist-info/licenses/LICENSE,sha256=yKBRwbO-cOPBrlpsZmJkkSa33DfY31aE8t7lZ0DwlUo,1071
315
+ runtimepy-5.15.8.dist-info/METADATA,sha256=FwB_Vl_qyjXiREOidJFwlPKEK0nCqvTrgq5ao57doHo,9268
316
+ runtimepy-5.15.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
317
+ runtimepy-5.15.8.dist-info/entry_points.txt,sha256=-btVBkYv7ybcopqZ_pRky-bEzu3vhbaG3W3Z7ERBiFE,51
318
+ runtimepy-5.15.8.dist-info/top_level.txt,sha256=0jPmh6yqHyyJJDwEID-LpQly-9kQ3WRMjH7Lix8peLg,10
319
+ runtimepy-5.15.8.dist-info/RECORD,,