servelive 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reymart Centeno
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: servelive
3
+ Version: 0.1.0
4
+ Summary: Static file server with no-cache headers and inotify-driven live reload over SSE
5
+ Author-email: Reymart Centeno <reymartcenteno03@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/groovyrey/servelive
8
+ Classifier: Environment :: Web Environment
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest; extra == "dev"
18
+ Requires-Dist: build; extra == "dev"
19
+ Requires-Dist: twine; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # servelive
23
+
24
+ A static file server with no-cache headers and live reload, powered by Linux
25
+ inotify and Server-Sent Events. A drop-in replacement for `python3 -m http.server`
26
+ that refreshes the browser the moment a served file changes. No polling anywhere,
27
+ and no third-party dependencies (pure Python standard library + the system libc).
28
+
29
+ ## Features
30
+
31
+ - No-cache headers on every response so dev browsers always fetch fresh files
32
+ - Live reload via **inotify** (real file-change detection) + **SSE** push
33
+ - Serves over the network by default (`0.0.0.0`) and prints your LAN address
34
+ - Degrades gracefully on systems without inotify (survives, just no reload)
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install servelive
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```bash
45
+ servelive # serve ./ on port 8000
46
+ servelive 8500 # serve ./ on port 8500
47
+ servelive 8500 ./site # serve ./site on port 8500
48
+ servelive --bind 127.0.0.1 9000 # localhost only
49
+ ```
50
+
51
+ Startup output mirrors a JS dev server:
52
+
53
+ ```
54
+ local: http://localhost:8000/
55
+ network: http://192.168.100.167:8000/
56
+ serving /home/you/site (live reload via inotify)
57
+ ```
58
+
59
+ Open the `local:` or `network:` URL in a browser, edit any file under the served
60
+ directory, and the page reloads itself.
61
+
62
+ ## How it works
63
+
64
+ 1. A background thread watches the served tree with the Linux `inotify` API
65
+ (called through `ctypes`, stdlib only).
66
+ 2. On a change, an 80 ms debounce coalesces the raw event burst into a single
67
+ notification.
68
+ 3. The server broadcasts a reload over an SSE stream to every connected page.
69
+
70
+ The reload script is injected into served HTML, so any page you open through
71
+ `servelive` gets live reload automatically.
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,54 @@
1
+ # servelive
2
+
3
+ A static file server with no-cache headers and live reload, powered by Linux
4
+ inotify and Server-Sent Events. A drop-in replacement for `python3 -m http.server`
5
+ that refreshes the browser the moment a served file changes. No polling anywhere,
6
+ and no third-party dependencies (pure Python standard library + the system libc).
7
+
8
+ ## Features
9
+
10
+ - No-cache headers on every response so dev browsers always fetch fresh files
11
+ - Live reload via **inotify** (real file-change detection) + **SSE** push
12
+ - Serves over the network by default (`0.0.0.0`) and prints your LAN address
13
+ - Degrades gracefully on systems without inotify (survives, just no reload)
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install servelive
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ servelive # serve ./ on port 8000
25
+ servelive 8500 # serve ./ on port 8500
26
+ servelive 8500 ./site # serve ./site on port 8500
27
+ servelive --bind 127.0.0.1 9000 # localhost only
28
+ ```
29
+
30
+ Startup output mirrors a JS dev server:
31
+
32
+ ```
33
+ local: http://localhost:8000/
34
+ network: http://192.168.100.167:8000/
35
+ serving /home/you/site (live reload via inotify)
36
+ ```
37
+
38
+ Open the `local:` or `network:` URL in a browser, edit any file under the served
39
+ directory, and the page reloads itself.
40
+
41
+ ## How it works
42
+
43
+ 1. A background thread watches the served tree with the Linux `inotify` API
44
+ (called through `ctypes`, stdlib only).
45
+ 2. On a change, an 80 ms debounce coalesces the raw event burst into a single
46
+ notification.
47
+ 3. The server broadcasts a reload over an SSE stream to every connected page.
48
+
49
+ The reload script is injected into served HTML, so any page you open through
50
+ `servelive` gets live reload automatically.
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "servelive"
7
+ version = "0.1.0"
8
+ description = "Static file server with no-cache headers and inotify-driven live reload over SSE"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Reymart Centeno", email = "reymartcenteno03@gmail.com" }]
13
+ classifiers = [
14
+ "Environment :: Web Environment",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: POSIX :: Linux",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
19
+ ]
20
+ # No runtime dependencies: pure stdlib (ctypes inotify, http.server).
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/groovyrey/servelive"
24
+
25
+ [project.scripts]
26
+ servelive = "servelive.cli:main"
27
+
28
+ [project.optional-dependencies]
29
+ dev = ["pytest", "build", "twine"]
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,14 @@
1
+ """servelive - static file server with inotify-driven live reload.
2
+
3
+ A drop-in replacement for ``python3 -m http.server`` that sends
4
+ ``Cache-Control: no-store`` so dev browsers always fetch fresh files, and
5
+ pushes an SSE "reload" event to connected pages whenever a served file changes.
6
+ File changes are detected with Linux inotify (via ctypes, no third-party
7
+ dependencies) so there is no polling anywhere.
8
+ """
9
+
10
+ from servelive.server import create_server
11
+ from servelive.cli import main
12
+ from servelive._version import __version__
13
+
14
+ __all__ = ["create_server", "main", "__version__"]
@@ -0,0 +1,4 @@
1
+ from servelive.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,98 @@
1
+ """Command-line entry point for servelive."""
2
+
3
+ import argparse
4
+ import socket
5
+ import subprocess
6
+ import sys
7
+
8
+ from servelive._version import __version__
9
+ from servelive.server import LiveServer
10
+ from servelive.watcher import is_watchable
11
+
12
+
13
+ def lan_ip():
14
+ """Determine the primary non-loopback IPv4 address on this host."""
15
+ try:
16
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
17
+ try:
18
+ s.connect(("8.8.8.8", 80))
19
+ ip = s.getsockname()[0]
20
+ finally:
21
+ s.close()
22
+ if ip and not ip.startswith("127."):
23
+ return ip
24
+ except OSError:
25
+ pass
26
+ try:
27
+ out = subprocess.run(
28
+ ["ip", "-4", "addr", "show", "scope", "global"],
29
+ capture_output=True, text=True,
30
+ ).stdout
31
+ for line in out.splitlines():
32
+ line = line.strip()
33
+ if line.startswith("inet "):
34
+ ip = line.split()[1].split("/")[0]
35
+ if not ip.startswith("127."):
36
+ return ip
37
+ except Exception:
38
+ pass
39
+ return None
40
+
41
+
42
+ def build_parser():
43
+ parser = argparse.ArgumentParser(
44
+ prog="servelive",
45
+ description="Static file server with no-cache headers and inotify-driven live reload.",
46
+ )
47
+ parser.add_argument(
48
+ "port", nargs="?", type=int, default=8000,
49
+ help="Port to bind (default: 8000).",
50
+ )
51
+ parser.add_argument(
52
+ "directory", nargs="?", default=".",
53
+ help="Directory to serve (default: current directory).",
54
+ )
55
+ parser.add_argument(
56
+ "--bind", "-b", default="0.0.0.0",
57
+ help="Address to bind (default: 0.0.0.0 so other devices can connect).",
58
+ )
59
+ parser.add_argument(
60
+ "--version", action="version", version=f"servelive {__version__}",
61
+ )
62
+ return parser
63
+
64
+
65
+ def main(argv=None):
66
+ args = build_parser().parse_args(argv)
67
+
68
+ if not (1 <= args.port <= 65535):
69
+ print(f"error: invalid port {args.port}", file=sys.stderr)
70
+ return 2
71
+
72
+ server = LiveServer(directory=args.directory, port=args.port)
73
+
74
+ watcher = "inotify" if is_watchable() else "none"
75
+ if watcher == "none":
76
+ print("warning: inotify unavailable - live reload disabled", file=sys.stderr)
77
+
78
+ try:
79
+ server.start()
80
+ except OSError as e:
81
+ print(f"error: could not bind {args.bind}:{args.port} ({e})", file=sys.stderr)
82
+ return 1
83
+
84
+ live = "live reload via inotify" if watcher == "inotify" else "live reload disabled"
85
+ local_host = "127.0.0.1" if args.bind not in ("0.0.0.0", "") else "localhost"
86
+ print(f" local: http://{local_host}:{args.port}/", flush=True)
87
+ if args.bind in ("0.0.0.0", ""):
88
+ nip = lan_ip()
89
+ if nip:
90
+ print(f" network: http://{nip}:{args.port}/", flush=True)
91
+ print(f" serving {server.directory} ({live})", flush=True)
92
+
93
+ server.serve_forever()
94
+ return 0
95
+
96
+
97
+ if __name__ == "__main__":
98
+ raise SystemExit(main())
@@ -0,0 +1,168 @@
1
+ """The static HTTP server with no-cache headers and SSE live reload."""
2
+
3
+ import functools
4
+ import http.server
5
+ import io
6
+ import os
7
+ import socketserver
8
+ import threading
9
+
10
+ from servelive.watcher import is_watchable, Watcher
11
+
12
+ LIVE_RELOAD = b"""<script>
13
+ (function () {
14
+ try {
15
+ var es = new EventSource(location.origin + "/__servelive_reload", { withCredentials: false });
16
+ es.onmessage = function (e) {
17
+ if (e.data === "reload") location.reload();
18
+ };
19
+ es.onerror = function () { es.close(); };
20
+ } catch (e) {}
21
+ })();
22
+ </script>
23
+ """
24
+
25
+ RELOAD_PATH = "/__servelive_reload"
26
+
27
+
28
+ class ReloadHub:
29
+ """Fan-out reload notifications to connected SSE clients."""
30
+
31
+ def __init__(self):
32
+ self._clients = []
33
+ self._lock = threading.Lock()
34
+
35
+ def add(self, event):
36
+ with self._lock:
37
+ self._clients.append(event)
38
+ return self._remove_client
39
+
40
+ def _remove_client(self, event):
41
+ with self._lock:
42
+ if event in self._clients:
43
+ self._clients.remove(event)
44
+
45
+ def broadcast(self):
46
+ with self._lock:
47
+ clients = list(self._clients)
48
+ for ev in clients:
49
+ ev.set()
50
+
51
+
52
+ class _SSEEvent(threading.Event):
53
+ pass
54
+
55
+
56
+ def sse_body(hub):
57
+ yield b"retry: 1000\n\n"
58
+ ev = _SSEEvent()
59
+ detach = hub.add(ev)
60
+ try:
61
+ while True:
62
+ if ev.wait(25):
63
+ ev.clear()
64
+ yield b"data: reload\n\n"
65
+ finally:
66
+ detach()
67
+
68
+
69
+ class NoCacheHandler(http.server.SimpleHTTPRequestHandler):
70
+ server_version = "servelive/1.1"
71
+
72
+ def end_headers(self):
73
+ self.send_header("Cache-Control", "no-store, no-cache, must-revalidate")
74
+ self.send_header("Pragma", "no-cache")
75
+ self.send_header("Expires", "0")
76
+ super().end_headers()
77
+
78
+ def do_GET(self):
79
+ if self.path.rstrip("/").endswith(RELOAD_PATH):
80
+ try:
81
+ self.send_response(200)
82
+ self.send_header("Content-Type", "text/event-stream")
83
+ self.send_header("Cache-Control", "no-cache")
84
+ self.send_header("Connection", "keep-alive")
85
+ self.send_header("X-Accel-Buffering", "no")
86
+ self.send_header("Access-Control-Allow-Origin", "*")
87
+ self.end_headers()
88
+ for chunk in sse_body(self.server.hub):
89
+ try:
90
+ self.wfile.write(chunk)
91
+ self.wfile.flush()
92
+ except (BrokenPipeError, ConnectionResetError, OSError):
93
+ break
94
+ except (BrokenPipeError, ConnectionResetError, OSError):
95
+ pass
96
+ return
97
+ return super().do_GET()
98
+
99
+ def send_head(self):
100
+ path = self.translate_path(self.path)
101
+ if os.path.isdir(path):
102
+ path = os.path.join(path, "index.html")
103
+ ctype = self.guess_type(path)
104
+ if ctype == "text/html" and os.path.exists(path):
105
+ try:
106
+ with open(path, "rb") as f:
107
+ html = f.read()
108
+ html = inject_reload_script(html)
109
+ self.send_response(200)
110
+ self.send_header("Content-Type", ctype or "text/html")
111
+ self.send_header("Content-Length", str(len(html)))
112
+ self.send_header("Last-Modified", self.date_time_string())
113
+ self.end_headers()
114
+ return io.BytesIO(html)
115
+ except OSError:
116
+ pass
117
+ return super().send_head()
118
+
119
+
120
+ def inject_reload_script(html):
121
+ if b"</body>" in html:
122
+ return html.replace(b"</body>", LIVE_RELOAD + b"</body>")
123
+ return html + LIVE_RELOAD
124
+
125
+
126
+ class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
127
+ daemon_threads = True
128
+ allow_reuse_address = True
129
+
130
+
131
+ class LiveServer:
132
+ """Convenience wrapper combining the HTTP server and the watcher."""
133
+
134
+ def __init__(self, directory=".", port=8000):
135
+ self.directory = os.path.abspath(directory)
136
+ self.port = int(port)
137
+ self.hub = ReloadHub()
138
+ self.watcher = None
139
+ self.httpd = None
140
+
141
+ def start(self):
142
+ handler = functools.partial(NoCacheHandler, directory=self.directory)
143
+ self.httpd = ThreadingHTTPServer(("", self.port), handler)
144
+ self.httpd.hub = self.hub
145
+ if is_watchable():
146
+ self.watcher = Watcher(self.directory, self.hub.broadcast).start()
147
+ return self
148
+
149
+ def serve_forever(self):
150
+ if self.httpd is None:
151
+ self.start()
152
+ try:
153
+ self.httpd.serve_forever()
154
+ except KeyboardInterrupt:
155
+ pass
156
+ finally:
157
+ self.close()
158
+
159
+ def close(self):
160
+ if self.watcher is not None:
161
+ self.watcher.close()
162
+ if self.httpd is not None:
163
+ self.httpd.server_close()
164
+
165
+
166
+ def create_server(directory=".", port=8000):
167
+ """Create (but do not start) a LiveServer."""
168
+ return LiveServer(directory=directory, port=port)
@@ -0,0 +1,186 @@
1
+ """inotify-backed change detection.
2
+
3
+ Watches a directory tree with the Linux inotify API through ctypes (stdlib
4
+ only). Calls a callback once per debounced change burst so consumers get a
5
+ single notification for what would otherwise be a burst of raw inotify events.
6
+ """
7
+
8
+ import ctypes
9
+ import ctypes.util
10
+ import errno
11
+ import os
12
+ import select
13
+ import struct
14
+ import threading
15
+ import time
16
+
17
+ try:
18
+ _libc_path = ctypes.util.find_library("c") or (
19
+ "/lib/libc.so.6" if os.path.exists("/lib/libc.so.6") else "libc.so.6"
20
+ )
21
+ libc = ctypes.CDLL(_libc_path, use_errno=True)
22
+ INOTIFY_OK = True
23
+ except Exception: # pragma: no cover - platform without libc
24
+ libc = None
25
+ INOTIFY_OK = False
26
+
27
+ IN_ATTRIB = 0x00000002
28
+ IN_CREATE = 0x00000100
29
+ IN_DELETE = 0x00000200
30
+ IN_MODIFY = 0x00000002
31
+ IN_MOVED_FROM = 0x00000040
32
+ IN_MOVED_TO = 0x00000080
33
+ IN_CLOSE_WRITE = 0x00000008
34
+
35
+ _WATCH_MASK = (
36
+ IN_ATTRIB
37
+ | IN_CREATE
38
+ | IN_DELETE
39
+ | IN_MODIFY
40
+ | IN_MOVED_FROM
41
+ | IN_MOVED_TO
42
+ | IN_CLOSE_WRITE
43
+ )
44
+
45
+ _SIZE_OF_INOTIFY_EVENT = 16
46
+ _NAME_MAX = 4096
47
+
48
+
49
+ def _setup_libc():
50
+ if not INOTIFY_OK:
51
+ return
52
+ libc.inotify_init.restype = ctypes.c_int
53
+ libc.inotify_add_watch.restype = ctypes.c_int
54
+ libc.inotify_add_watch.argtypes = [
55
+ ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32,
56
+ ]
57
+ libc.inotify_rm_watch.argtypes = [ctypes.c_int, ctypes.c_int]
58
+ libc.read.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t]
59
+ libc.read.restype = ctypes.c_ssize_t
60
+ libc.close.argtypes = [ctypes.c_int]
61
+
62
+
63
+ _setup_libc()
64
+
65
+
66
+ def is_watchable():
67
+ """Return True if inotify is available on this system."""
68
+ return INOTIFY_OK
69
+
70
+
71
+ class Watcher:
72
+ """Watch a directory tree and call ``on_change`` per debounced change."""
73
+
74
+ def __init__(self, root, on_change, debounce_ms=80, poll_ms=250):
75
+ self.root = root
76
+ self.on_change = on_change
77
+ self.debounce_ms = debounce_ms / 1000.0
78
+ self.poll_ms = poll_ms
79
+ self._fd = None
80
+ self._wd_to_path = {}
81
+
82
+ def start(self):
83
+ """Start watching. Returns self, or None if inotify is unavailable."""
84
+ if not INOTIFY_OK:
85
+ return None
86
+ fd = libc.inotify_init()
87
+ if fd < 0:
88
+ return None
89
+ self._fd = fd
90
+
91
+ mask = _WATCH_MASK
92
+ poll = select.poll()
93
+ poll.register(fd, select.POLLIN)
94
+ buf = ctypes.create_string_buffer(_NAME_MAX * 4)
95
+
96
+ def add_watch(path):
97
+ wd = libc.inotify_add_watch(fd, os.fsencode(path), mask)
98
+ if wd >= 0:
99
+ self._wd_to_path[wd] = path
100
+ return wd
101
+
102
+ def add_tree(base):
103
+ for dirpath, dirnames, _ in os.walk(base):
104
+ for d in list(dirnames):
105
+ try:
106
+ add_watch(os.path.join(dirpath, d))
107
+ except OSError:
108
+ pass
109
+
110
+ root_wd = add_watch(self.root)
111
+ if root_wd < 0:
112
+ libc.close(fd)
113
+ self._fd = None
114
+ return None
115
+ add_tree(self.root)
116
+
117
+ pending = {"flag": False}
118
+ lock = threading.Lock()
119
+ debounce = threading.Event()
120
+
121
+ def mark_changed():
122
+ with lock:
123
+ pending["flag"] = True
124
+ debounce.set()
125
+
126
+ def broadcaster():
127
+ while True:
128
+ debounce.wait()
129
+ debounce.clear()
130
+ time.sleep(self.debounce_ms)
131
+ with lock:
132
+ if not pending["flag"]:
133
+ continue
134
+ pending["flag"] = False
135
+ self.on_change()
136
+
137
+ threading.Thread(target=broadcaster, daemon=True).start()
138
+
139
+ def loop():
140
+ while True:
141
+ if not poll.poll(self.poll_ms):
142
+ continue
143
+ while True:
144
+ n = libc.read(fd, buf, len(buf))
145
+ if n < 0:
146
+ err = ctypes.get_errno()
147
+ if err in (errno.EAGAIN, errno.EINTR):
148
+ break
149
+ continue
150
+ if n == 0:
151
+ break
152
+ off = 0
153
+ while off < n:
154
+ wd, maskv, _cookie, name_len = struct.unpack_from(
155
+ "iIII", buf, off
156
+ )
157
+ off += _SIZE_OF_INOTIFY_EVENT
158
+ name = (
159
+ buf.raw[off:off + name_len]
160
+ .split(b"\x00", 1)[0]
161
+ .decode("utf-8", "replace")
162
+ if name_len
163
+ else ""
164
+ )
165
+ off += name_len
166
+ dirname = self._wd_to_path.get(wd, self.root)
167
+ if (maskv & (IN_CREATE | IN_MOVED_TO)) and name:
168
+ newpath = os.path.join(dirname, name)
169
+ if os.path.isdir(newpath):
170
+ try:
171
+ add_watch(newpath)
172
+ except OSError:
173
+ pass
174
+ mark_changed()
175
+
176
+ threading.Thread(target=loop, daemon=True).start()
177
+ return self
178
+
179
+ def close(self):
180
+ """Stop watching and release the inotify fd."""
181
+ if self._fd is not None:
182
+ try:
183
+ libc.close(self._fd)
184
+ except Exception:
185
+ pass
186
+ self._fd = None
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: servelive
3
+ Version: 0.1.0
4
+ Summary: Static file server with no-cache headers and inotify-driven live reload over SSE
5
+ Author-email: Reymart Centeno <reymartcenteno03@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/groovyrey/servelive
8
+ Classifier: Environment :: Web Environment
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest; extra == "dev"
18
+ Requires-Dist: build; extra == "dev"
19
+ Requires-Dist: twine; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # servelive
23
+
24
+ A static file server with no-cache headers and live reload, powered by Linux
25
+ inotify and Server-Sent Events. A drop-in replacement for `python3 -m http.server`
26
+ that refreshes the browser the moment a served file changes. No polling anywhere,
27
+ and no third-party dependencies (pure Python standard library + the system libc).
28
+
29
+ ## Features
30
+
31
+ - No-cache headers on every response so dev browsers always fetch fresh files
32
+ - Live reload via **inotify** (real file-change detection) + **SSE** push
33
+ - Serves over the network by default (`0.0.0.0`) and prints your LAN address
34
+ - Degrades gracefully on systems without inotify (survives, just no reload)
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install servelive
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```bash
45
+ servelive # serve ./ on port 8000
46
+ servelive 8500 # serve ./ on port 8500
47
+ servelive 8500 ./site # serve ./site on port 8500
48
+ servelive --bind 127.0.0.1 9000 # localhost only
49
+ ```
50
+
51
+ Startup output mirrors a JS dev server:
52
+
53
+ ```
54
+ local: http://localhost:8000/
55
+ network: http://192.168.100.167:8000/
56
+ serving /home/you/site (live reload via inotify)
57
+ ```
58
+
59
+ Open the `local:` or `network:` URL in a browser, edit any file under the served
60
+ directory, and the page reloads itself.
61
+
62
+ ## How it works
63
+
64
+ 1. A background thread watches the served tree with the Linux `inotify` API
65
+ (called through `ctypes`, stdlib only).
66
+ 2. On a change, an 80 ms debounce coalesces the raw event burst into a single
67
+ notification.
68
+ 3. The server broadcasts a reload over an SSE stream to every connected page.
69
+
70
+ The reload script is injected into served HTML, so any page you open through
71
+ `servelive` gets live reload automatically.
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/servelive/__init__.py
5
+ src/servelive/__main__.py
6
+ src/servelive/_version.py
7
+ src/servelive/cli.py
8
+ src/servelive/server.py
9
+ src/servelive/watcher.py
10
+ src/servelive.egg-info/PKG-INFO
11
+ src/servelive.egg-info/SOURCES.txt
12
+ src/servelive.egg-info/dependency_links.txt
13
+ src/servelive.egg-info/entry_points.txt
14
+ src/servelive.egg-info/requires.txt
15
+ src/servelive.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ servelive = servelive.cli:main
@@ -0,0 +1,5 @@
1
+
2
+ [dev]
3
+ pytest
4
+ build
5
+ twine
@@ -0,0 +1 @@
1
+ servelive