All of lore.kernel.org
 help / color / mirror / Atom feed
* [Buildroot] [PATCH] package/python-servestatic: new package
@ 2026-09-07  6:08 Sebastian Michel
  2026-10-03 17:34 ` Thomas Petazzoni via buildroot
  0 siblings, 1 reply; 2+ messages in thread
From: Sebastian Michel @ 2026-09-07  6:08 UTC (permalink / raw)
  To: buildroot; +Cc: James Hilliard, Thomas Petazzoni, Sebastian Michel

Added package for ServeStatic, version 4.3.2.
https://github.com/Archmonger/ServeStatic

Added a runtime test that verifies static-file responses through both
the WSGI and ASGI interfaces. Also verified that the installed
servestatic command-line entry point starts successfully.

Signed-off-by: Sebastian Michel <sebastian.michel@oss.othermo.de>
---
 DEVELOPERS                                    |   5 +
 package/Config.in                             |   1 +
 package/python-servestatic/Config.in          |  11 ++
 .../python-servestatic.hash                   |   3 +
 .../python-servestatic/python-servestatic.mk  |  13 ++
 .../package/sample_python_servestatic.py      | 118 ++++++++++++++++++
 .../tests/package/test_python_servestatic.py  |  19 +++
 7 files changed, 170 insertions(+)
 create mode 100644 package/python-servestatic/Config.in
 create mode 100644 package/python-servestatic/python-servestatic.hash
 create mode 100644 package/python-servestatic/python-servestatic.mk
 create mode 100644 support/testing/tests/package/sample_python_servestatic.py
 create mode 100644 support/testing/tests/package/test_python_servestatic.py

diff --git a/DEVELOPERS b/DEVELOPERS
index 53104cc60b..4e3dfb370c 100644
--- a/DEVELOPERS
+++ b/DEVELOPERS
@@ -3001,6 +3001,11 @@ F:	package/lrzip/
 N:	Sam Voss <sam.voss@gmail.com>
 F:	package/ripgrep/
 
+N:	Sebastian Michel <sebastian.michel@oss.othermo.de>
+F:	package/python-servestatic/
+F:	support/testing/tests/package/sample_python_servestatic.py
+F:	support/testing/tests/package/test_python_servestatic.py
+
 N:	Sébastien Szymanski <sebastien.szymanski@armadeus.com>
 F:	board/armadeus/
 F:	configs/armadeus*
diff --git a/package/Config.in b/package/Config.in
index 6058800682..79871aefd4 100644
--- a/package/Config.in
+++ b/package/Config.in
@@ -1418,6 +1418,7 @@ menu "External python modules"
 	source "package/python-sentry-sdk/Config.in"
 	source "package/python-serial/Config.in"
 	source "package/python-serial-asyncio/Config.in"
+	source "package/python-servestatic/Config.in"
 	source "package/python-service-identity/Config.in"
 	source "package/python-setproctitle/Config.in"
 	source "package/python-setuptools/Config.in"
diff --git a/package/python-servestatic/Config.in b/package/python-servestatic/Config.in
new file mode 100644
index 0000000000..b2a1e2a45d
--- /dev/null
+++ b/package/python-servestatic/Config.in
@@ -0,0 +1,11 @@
+config BR2_PACKAGE_PYTHON_SERVESTATIC
+	bool "python-servestatic"
+	select BR2_PACKAGE_PYTHON_ASGIREF # runtime dependency
+	select BR2_PACKAGE_PYTHON3_ZLIB # runtime dependency for cli / servestatic.storage usage
+	help
+	  Production-grade static file server for Python WSGI and ASGI.
+
+	  ServeStatic runs standalone or as middleware for an existing
+	  WSGI, ASGI, or Django application.
+
+	  https://github.com/Archmonger/ServeStatic
diff --git a/package/python-servestatic/python-servestatic.hash b/package/python-servestatic/python-servestatic.hash
new file mode 100644
index 0000000000..6d1fb6ecf9
--- /dev/null
+++ b/package/python-servestatic/python-servestatic.hash
@@ -0,0 +1,3 @@
+# Locally computed sha256 checksums
+sha256  1ab67d0654910c000864f9b6d7fefa8befc21a2156d539cf9e519afcc27c4d63  python-servestatic-4.3.2.tar.gz
+sha256  1770e2fc440cce75b2b2bd1580c6d59f58a497f0bb7abe91a0136bee0f688bf3  LICENSE.md
diff --git a/package/python-servestatic/python-servestatic.mk b/package/python-servestatic/python-servestatic.mk
new file mode 100644
index 0000000000..26d93fe9b5
--- /dev/null
+++ b/package/python-servestatic/python-servestatic.mk
@@ -0,0 +1,13 @@
+################################################################################
+#
+# python-servestatic
+#
+################################################################################
+
+PYTHON_SERVESTATIC_VERSION = 4.3.2
+PYTHON_SERVESTATIC_SITE = $(call github,Archmonger,ServeStatic,$(PYTHON_SERVESTATIC_VERSION))
+PYTHON_SERVESTATIC_SETUP_TYPE = hatch
+PYTHON_SERVESTATIC_LICENSE = MIT
+PYTHON_SERVESTATIC_LICENSE_FILES = LICENSE.md
+
+$(eval $(python-package))
diff --git a/support/testing/tests/package/sample_python_servestatic.py b/support/testing/tests/package/sample_python_servestatic.py
new file mode 100644
index 0000000000..22c69cdbcf
--- /dev/null
+++ b/support/testing/tests/package/sample_python_servestatic.py
@@ -0,0 +1,118 @@
+import asyncio
+import os
+import tempfile
+from collections.abc import Callable
+from types import TracebackType
+from typing import TypeAlias
+from wsgiref.types import StartResponse
+
+from asgiref.typing import HTTPScope, HTTPRequestEvent, ASGIVersions
+
+from servestatic import ServeStatic, ServeStaticASGI
+
+CONTENT = b"ServeStatic works on Buildroot.\n"
+
+_ExcInfo: TypeAlias = tuple[type[BaseException], BaseException, TracebackType]
+_OptExcInfo: TypeAlias = _ExcInfo | tuple[None, None, None]
+
+
+def assert_response_ok(status, headers, body):
+    assert status == 200
+    assert headers["content-length"] == str(len(CONTENT))
+    assert headers["content-type"].startswith("text/plain")
+    assert body == CONTENT
+
+
+def test_wsgi(content_directory):
+    app = ServeStatic(None, root=content_directory)
+    response_data = {}
+
+    class DummyStartResponse(StartResponse):
+        def __call__(self, status: str, headers: list[tuple[str, str]], exc_info: _OptExcInfo | None = None, /) -> \
+                Callable[[bytes], object]:
+            response_data["status"] = int(status.split()[0])
+            response_data["headers"] = {
+                key.lower(): value for key, value in headers
+            }
+            return super().__call__(status, headers, exc_info)
+
+    start_response = DummyStartResponse()
+
+    environ_test_request = {
+        "PATH_INFO": "/test_content.txt",
+        "REQUEST_METHOD": "GET",
+    }
+    response = app(environ_test_request, start_response)
+    try:
+        body = b"".join(response)
+    finally:
+        close = getattr(response, "close", None)
+        if callable(close):
+            close: Callable[None]
+            close()
+
+    assert_response_ok(
+        response_data["status"], response_data["headers"], body
+    )
+
+
+async def test_asgi(content_directory):
+    app = ServeStaticASGI(None, root=content_directory)
+    messages_received_from_application = []
+
+    async def receive() -> HTTPRequestEvent:
+        return HTTPRequestEvent(
+            type="http.request",
+            body=b"",
+            more_body=False,
+        )
+
+    # will be invoked when the application "sends" data back -> append all sent data to the messages list
+    async def send(message):
+        messages_received_from_application.append(message)
+
+    # run the test request
+    scope_test_request = HTTPScope(
+        type="http",
+        asgi=ASGIVersions(
+            version="3.0",
+            spec_version="2.5"
+        ),
+        http_version="1.1",
+        method="GET",
+        scheme="http",
+        path="/test_content.txt",
+        raw_path=b"/hello.txt",
+        query_string=b"",
+        root_path="",
+        headers=[],
+        client=("127.0.0.1", 12345),
+        server=("127.0.0.1", 80),
+        extensions=None
+    )
+    await app(scope_test_request, receive, send)
+
+    # first segment contains the headers
+    message_with_headers = messages_received_from_application[0]
+
+    # convert headers into correct form for assert_response_ok
+    headers = {
+        key.decode(): value.decode() for key, value in message_with_headers["headers"]
+    }
+
+    # collect content of all "body" type messages
+    body = b"".join(
+        message.get("body", b"")
+        for message in messages_received_from_application
+        if message["type"] == "http.response.body"
+    )
+
+    # assert everything works as expected
+    assert_response_ok(message_with_headers["status"], headers, body)
+
+
+with tempfile.TemporaryDirectory() as tmp_content_directory:
+    with open(os.path.join(tmp_content_directory, "test_content.txt"), "wb") as static_file:
+        static_file.write(CONTENT)
+    test_wsgi(tmp_content_directory)
+    asyncio.run(test_asgi(tmp_content_directory))
diff --git a/support/testing/tests/package/test_python_servestatic.py b/support/testing/tests/package/test_python_servestatic.py
new file mode 100644
index 0000000000..d9c762962a
--- /dev/null
+++ b/support/testing/tests/package/test_python_servestatic.py
@@ -0,0 +1,19 @@
+from tests.package.test_python import TestPythonPackageBase
+
+
+class TestPythonServeStatic(TestPythonPackageBase):
+    __test__ = True
+    config = TestPythonPackageBase.config + \
+        """
+        BR2_PACKAGE_PYTHON3=y
+        BR2_PACKAGE_PYTHON_SERVESTATIC=y
+        """
+
+    sample_scripts = ["tests/package/sample_python_servestatic.py"]
+
+    def run_sample_scripts(self):
+        self.assertRunOk(
+            "servestatic --help | grep -q 'Process static files'"
+        )
+
+        super().run_sample_scripts()
-- 
2.55.0

_______________________________________________
buildroot mailing list
buildroot@buildroot.org
https://lists.buildroot.org/mailman/listinfo/buildroot

^ permalink raw reply related	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-10-03 17:34 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-07  6:08 [Buildroot] [PATCH] package/python-servestatic: new package Sebastian Michel
2026-10-03 17:34 ` Thomas Petazzoni via buildroot

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.