webdev 2.0.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.
- webdev/__init__.py +30 -0
- webdev/__main__.py +29 -0
- webdev/cli.py +330 -0
- webdev/config.py +210 -0
- webdev/index.py +298 -0
- webdev/livereload.py +169 -0
- webdev/net.py +145 -0
- webdev/run.py +124 -0
- webdev/server.py +336 -0
- webdev/static.py +195 -0
- webdev/templates/dark/base.html.jinja2 +17 -0
- webdev/templates/dark/index.html.jinja2 +71 -0
- webdev/templates/dark/partials/sort.js.jinja2 +52 -0
- webdev/templates/dark/partials/styles.css.jinja2 +110 -0
- webdev/tls.py +159 -0
- webdev-2.0.0.dist-info/METADATA +240 -0
- webdev-2.0.0.dist-info/RECORD +20 -0
- webdev-2.0.0.dist-info/WHEEL +4 -0
- webdev-2.0.0.dist-info/entry_points.txt +2 -0
- webdev-2.0.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{% extends theme ~ "/base.html.jinja2" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<header class="head">
|
|
5
|
+
<nav class="crumbs" aria-label="Breadcrumb">
|
|
6
|
+
{% for crumb in breadcrumbs %}
|
|
7
|
+
{% if loop.last %}
|
|
8
|
+
<span class="crumb current">{{ crumb.name }}</span>
|
|
9
|
+
{% else %}
|
|
10
|
+
<a class="crumb" href="{{ crumb.href }}">{{ crumb.name }}</a>
|
|
11
|
+
<span class="sep">/</span>
|
|
12
|
+
{% endif %}
|
|
13
|
+
{% endfor %}
|
|
14
|
+
</nav>
|
|
15
|
+
<p class="meta">{{ n_dirs }} director{{ "y" if n_dirs == 1 else "ies" }},
|
|
16
|
+
{{ n_files }} file{{ "" if n_files == 1 else "s" }}</p>
|
|
17
|
+
</header>
|
|
18
|
+
|
|
19
|
+
<main>
|
|
20
|
+
<table class="listing" id="listing">
|
|
21
|
+
<thead>
|
|
22
|
+
<tr>
|
|
23
|
+
<th class="col-name" data-sort="name" aria-sort="ascending">Name</th>
|
|
24
|
+
<th class="col-size" data-sort="size">Size</th>
|
|
25
|
+
<th class="col-date" data-sort="date">Modified</th>
|
|
26
|
+
</tr>
|
|
27
|
+
</thead>
|
|
28
|
+
<tbody>
|
|
29
|
+
{% if not is_root %}
|
|
30
|
+
<tr class="row up">
|
|
31
|
+
<td class="col-name">
|
|
32
|
+
<span class="icon icon-up" aria-hidden="true"></span>
|
|
33
|
+
<a href="../">..</a>
|
|
34
|
+
</td>
|
|
35
|
+
<td class="col-size"></td>
|
|
36
|
+
<td class="col-date"></td>
|
|
37
|
+
</tr>
|
|
38
|
+
{% endif %}
|
|
39
|
+
{% for entry in entries %}
|
|
40
|
+
<tr class="row {{ 'is-dir' if entry.is_dir else 'is-file' }}">
|
|
41
|
+
<td class="col-name">
|
|
42
|
+
<span class="icon icon-{{ entry.icon }}" aria-hidden="true"></span>
|
|
43
|
+
<a href="{{ entry.href }}">{{ entry.name }}{{ "/" if entry.is_dir }}</a>
|
|
44
|
+
</td>
|
|
45
|
+
<td class="col-size" data-size="{{ entry.size if entry.size is not none else -1 }}">
|
|
46
|
+
{{ entry.size_human }}
|
|
47
|
+
</td>
|
|
48
|
+
<td class="col-date" data-time="{{ entry.mtime }}">{{ entry.mtime_human }}</td>
|
|
49
|
+
</tr>
|
|
50
|
+
{% endfor %}
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
{% if not entries and is_root %}
|
|
54
|
+
<p class="empty">This directory is empty.</p>
|
|
55
|
+
{% elif not entries %}
|
|
56
|
+
<p class="empty">This directory is empty.</p>
|
|
57
|
+
{% endif %}
|
|
58
|
+
</main>
|
|
59
|
+
|
|
60
|
+
<footer class="foot">
|
|
61
|
+
<span>webdev{% if version %} <span class="ver">v{{ version }}</span>{% endif %}</span>
|
|
62
|
+
<span class="dot">·</span>
|
|
63
|
+
<span>serving {{ served_root }}</span>
|
|
64
|
+
</footer>
|
|
65
|
+
{% endblock %}
|
|
66
|
+
|
|
67
|
+
{% block scripts %}
|
|
68
|
+
<script>
|
|
69
|
+
{% include theme ~ "/partials/sort.js.jinja2" %}
|
|
70
|
+
</script>
|
|
71
|
+
{% endblock %}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
var table = document.getElementById("listing");
|
|
3
|
+
if (!table) return;
|
|
4
|
+
var tbody = table.tBodies[0];
|
|
5
|
+
var headers = table.tHead.rows[0].cells;
|
|
6
|
+
|
|
7
|
+
function cellValue(row, key) {
|
|
8
|
+
if (key === "name") {
|
|
9
|
+
var link = row.querySelector(".col-name a");
|
|
10
|
+
return (link ? link.textContent : "").toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
if (key === "size") {
|
|
13
|
+
var s = row.querySelector(".col-size");
|
|
14
|
+
return parseFloat(s && s.dataset.size != null ? s.dataset.size : -1);
|
|
15
|
+
}
|
|
16
|
+
if (key === "date") {
|
|
17
|
+
var d = row.querySelector(".col-date");
|
|
18
|
+
return parseFloat(d && d.dataset.time != null ? d.dataset.time : 0);
|
|
19
|
+
}
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sortBy(key, ascending) {
|
|
24
|
+
var rows = Array.prototype.slice.call(tbody.rows);
|
|
25
|
+
// Keep the ".." row pinned to the top.
|
|
26
|
+
var up = rows.filter(function (r) { return r.classList.contains("up"); });
|
|
27
|
+
var items = rows.filter(function (r) { return !r.classList.contains("up"); });
|
|
28
|
+
items.sort(function (a, b) {
|
|
29
|
+
var va = cellValue(a, key), vb = cellValue(b, key);
|
|
30
|
+
if (va < vb) return ascending ? -1 : 1;
|
|
31
|
+
if (va > vb) return ascending ? 1 : -1;
|
|
32
|
+
return 0;
|
|
33
|
+
});
|
|
34
|
+
up.concat(items).forEach(function (r) { tbody.appendChild(r); });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (var i = 0; i < headers.length; i++) {
|
|
38
|
+
(function (th) {
|
|
39
|
+
var key = th.getAttribute("data-sort");
|
|
40
|
+
if (!key) return;
|
|
41
|
+
th.addEventListener("click", function () {
|
|
42
|
+
var current = th.getAttribute("aria-sort");
|
|
43
|
+
var ascending = current !== "ascending";
|
|
44
|
+
for (var j = 0; j < headers.length; j++) {
|
|
45
|
+
headers[j].removeAttribute("aria-sort");
|
|
46
|
+
}
|
|
47
|
+
th.setAttribute("aria-sort", ascending ? "ascending" : "descending");
|
|
48
|
+
sortBy(key, ascending);
|
|
49
|
+
});
|
|
50
|
+
})(headers[i]);
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0d1117;
|
|
3
|
+
--bg-elev: #161b22;
|
|
4
|
+
--bg-hover: #1f2630;
|
|
5
|
+
--border: #30363d;
|
|
6
|
+
--text: #e6edf3;
|
|
7
|
+
--text-dim: #9198a1;
|
|
8
|
+
--accent: #4493f8;
|
|
9
|
+
--accent-dim: #388bfd;
|
|
10
|
+
--dir: #d29922;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
* { box-sizing: border-box; }
|
|
14
|
+
|
|
15
|
+
html, body {
|
|
16
|
+
margin: 0;
|
|
17
|
+
padding: 0;
|
|
18
|
+
background: var(--bg);
|
|
19
|
+
color: var(--text);
|
|
20
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
21
|
+
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
22
|
+
font-size: 14px;
|
|
23
|
+
line-height: 1.5;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.wrap {
|
|
27
|
+
max-width: 980px;
|
|
28
|
+
margin: 0 auto;
|
|
29
|
+
padding: 24px 20px 48px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.head { margin-bottom: 16px; }
|
|
33
|
+
|
|
34
|
+
.crumbs {
|
|
35
|
+
font-size: 18px;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
word-break: break-all;
|
|
38
|
+
}
|
|
39
|
+
.crumb { color: var(--accent); text-decoration: none; }
|
|
40
|
+
.crumb:hover { text-decoration: underline; }
|
|
41
|
+
.crumb.current { color: var(--text); }
|
|
42
|
+
.sep { color: var(--text-dim); margin: 0 2px; }
|
|
43
|
+
|
|
44
|
+
.meta { color: var(--text-dim); margin: 4px 0 0; font-size: 13px; }
|
|
45
|
+
|
|
46
|
+
.listing {
|
|
47
|
+
width: 100%;
|
|
48
|
+
border-collapse: collapse;
|
|
49
|
+
background: var(--bg-elev);
|
|
50
|
+
border: 1px solid var(--border);
|
|
51
|
+
border-radius: 10px;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.listing thead th {
|
|
56
|
+
text-align: left;
|
|
57
|
+
font-weight: 600;
|
|
58
|
+
color: var(--text-dim);
|
|
59
|
+
padding: 10px 14px;
|
|
60
|
+
border-bottom: 1px solid var(--border);
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
user-select: none;
|
|
63
|
+
white-space: nowrap;
|
|
64
|
+
}
|
|
65
|
+
.listing thead th:hover { color: var(--text); }
|
|
66
|
+
.listing thead th[aria-sort="ascending"]::after { content: " \2191"; }
|
|
67
|
+
.listing thead th[aria-sort="descending"]::after { content: " \2193"; }
|
|
68
|
+
|
|
69
|
+
.listing td {
|
|
70
|
+
padding: 8px 14px;
|
|
71
|
+
border-bottom: 1px solid var(--border);
|
|
72
|
+
vertical-align: middle;
|
|
73
|
+
}
|
|
74
|
+
.listing tbody tr:last-child td { border-bottom: none; }
|
|
75
|
+
.listing tbody tr:hover { background: var(--bg-hover); }
|
|
76
|
+
|
|
77
|
+
.col-size, .col-date { color: var(--text-dim); white-space: nowrap; }
|
|
78
|
+
.col-size { text-align: right; width: 110px; }
|
|
79
|
+
.col-date { width: 160px; }
|
|
80
|
+
|
|
81
|
+
.col-name a { color: var(--text); text-decoration: none; }
|
|
82
|
+
.col-name a:hover { color: var(--accent); text-decoration: underline; }
|
|
83
|
+
.is-dir .col-name a { color: var(--accent); font-weight: 500; }
|
|
84
|
+
|
|
85
|
+
.icon {
|
|
86
|
+
display: inline-block;
|
|
87
|
+
width: 1.1em;
|
|
88
|
+
margin-right: 8px;
|
|
89
|
+
text-align: center;
|
|
90
|
+
}
|
|
91
|
+
.icon::before { color: var(--text-dim); }
|
|
92
|
+
.icon-dir::before { content: "\1F4C1"; }
|
|
93
|
+
.icon-up::before { content: "\2B06"; }
|
|
94
|
+
.icon-file::before { content: "\1F4C4"; }
|
|
95
|
+
.icon-image::before { content: "\1F5BC"; }
|
|
96
|
+
.icon-video::before { content: "\1F3AC"; }
|
|
97
|
+
.icon-audio::before { content: "\1F3B5"; }
|
|
98
|
+
.icon-code::before { content: "\1F4C3"; }
|
|
99
|
+
.icon-archive::before { content: "\1F4E6"; }
|
|
100
|
+
.icon-doc::before { content: "\1F4C4"; }
|
|
101
|
+
|
|
102
|
+
.empty { color: var(--text-dim); padding: 24px 2px; }
|
|
103
|
+
|
|
104
|
+
.foot {
|
|
105
|
+
margin-top: 18px;
|
|
106
|
+
color: var(--text-dim);
|
|
107
|
+
font-size: 12px;
|
|
108
|
+
}
|
|
109
|
+
.foot .dot { margin: 0 6px; }
|
|
110
|
+
.foot .ver { font-variant-numeric: tabular-nums; opacity: 0.8; }
|
webdev/tls.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Copyright (C) 2026 KuraLabs S.R.L <info@kuralabs.io>
|
|
2
|
+
# Copyright (C) 2015-2016 Carlos Jenkins <carlos@jenkins.co.cr>
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing,
|
|
11
|
+
# software distributed under the License is distributed on an
|
|
12
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
13
|
+
# KIND, either express or implied. See the License for the
|
|
14
|
+
# specific language governing permissions and limitations
|
|
15
|
+
# under the License.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
Optional self-signed TLS certificate generation for ``--https``.
|
|
19
|
+
|
|
20
|
+
If the user provides ``--cert``/``--key`` those are used directly. Otherwise a
|
|
21
|
+
throwaway self-signed certificate is generated into a temporary directory. We
|
|
22
|
+
avoid adding a hard cryptography dependency: generation uses the ``cryptography``
|
|
23
|
+
package when present, falling back to the system ``openssl`` binary. If neither
|
|
24
|
+
is available a clear error asks the user to pass ``--cert``/``--key``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import datetime
|
|
30
|
+
import subprocess
|
|
31
|
+
import tempfile
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TLSUnavailableError(Exception):
|
|
36
|
+
"""
|
|
37
|
+
Raised when a self-signed certificate cannot be generated.
|
|
38
|
+
|
|
39
|
+
Signals that neither the ``cryptography`` package nor the ``openssl``
|
|
40
|
+
binary were available to produce a throwaway certificate.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _generate_with_cryptography(cert_path: Path, key_path: Path) -> bool:
|
|
45
|
+
"""
|
|
46
|
+
Generate a self-signed certificate using the ``cryptography`` package.
|
|
47
|
+
|
|
48
|
+
Attempts to import ``cryptography``; if present, builds an RSA key and a
|
|
49
|
+
one-year self-signed certificate for ``localhost`` and writes both files.
|
|
50
|
+
|
|
51
|
+
:param cert_path: Destination path for the PEM-encoded certificate.
|
|
52
|
+
:param key_path: Destination path for the PEM-encoded private key.
|
|
53
|
+
:return: ``True`` if the files were written, ``False`` if the package is
|
|
54
|
+
not installed.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
from cryptography import x509
|
|
58
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
59
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
60
|
+
from cryptography.x509.oid import NameOID
|
|
61
|
+
except ModuleNotFoundError:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
65
|
+
name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, 'localhost')])
|
|
66
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
67
|
+
cert = (
|
|
68
|
+
x509.CertificateBuilder()
|
|
69
|
+
.subject_name(name)
|
|
70
|
+
.issuer_name(name)
|
|
71
|
+
.public_key(key.public_key())
|
|
72
|
+
.serial_number(x509.random_serial_number())
|
|
73
|
+
.not_valid_before(now - datetime.timedelta(days=1))
|
|
74
|
+
.not_valid_after(now + datetime.timedelta(days=365))
|
|
75
|
+
.add_extension(
|
|
76
|
+
x509.SubjectAlternativeName([x509.DNSName('localhost')]),
|
|
77
|
+
critical=False,
|
|
78
|
+
)
|
|
79
|
+
.sign(key, hashes.SHA256())
|
|
80
|
+
)
|
|
81
|
+
key_path.write_bytes(
|
|
82
|
+
key.private_bytes(
|
|
83
|
+
encoding=serialization.Encoding.PEM,
|
|
84
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
85
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _generate_with_openssl(cert_path: Path, key_path: Path) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
Generate a self-signed certificate using the system ``openssl`` binary.
|
|
95
|
+
|
|
96
|
+
Invokes ``openssl req -x509`` to create a one-year self-signed certificate
|
|
97
|
+
for ``localhost`` without a passphrase.
|
|
98
|
+
|
|
99
|
+
:param cert_path: Destination path for the PEM-encoded certificate.
|
|
100
|
+
:param key_path: Destination path for the PEM-encoded private key.
|
|
101
|
+
:return: ``True`` if both files were produced, ``False`` if ``openssl`` is
|
|
102
|
+
missing or the command failed.
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
subprocess.run(
|
|
106
|
+
[
|
|
107
|
+
'openssl',
|
|
108
|
+
'req',
|
|
109
|
+
'-x509',
|
|
110
|
+
'-newkey',
|
|
111
|
+
'rsa:2048',
|
|
112
|
+
'-keyout',
|
|
113
|
+
str(key_path),
|
|
114
|
+
'-out',
|
|
115
|
+
str(cert_path),
|
|
116
|
+
'-days',
|
|
117
|
+
'365',
|
|
118
|
+
'-nodes',
|
|
119
|
+
'-subj',
|
|
120
|
+
'/CN=localhost',
|
|
121
|
+
],
|
|
122
|
+
check=True,
|
|
123
|
+
capture_output=True,
|
|
124
|
+
)
|
|
125
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
126
|
+
return False
|
|
127
|
+
return cert_path.is_file() and key_path.is_file()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def ensure_tls_files(cert: str | None, key: str | None) -> tuple[str, str]:
|
|
131
|
+
"""
|
|
132
|
+
Return a ``(cert_path, key_path)`` pair for HTTPS.
|
|
133
|
+
|
|
134
|
+
If both *cert* and *key* are provided they are returned as-is. Otherwise a
|
|
135
|
+
self-signed certificate is generated into a temporary directory, preferring
|
|
136
|
+
the ``cryptography`` package and falling back to the ``openssl`` binary.
|
|
137
|
+
|
|
138
|
+
:param cert: Path to an existing certificate file, or ``None``.
|
|
139
|
+
:param key: Path to an existing private key file, or ``None``.
|
|
140
|
+
:return: A tuple of ``(cert_path, key_path)`` filesystem paths.
|
|
141
|
+
:raises TLSUnavailableError: If no certificate could be generated because
|
|
142
|
+
neither ``cryptography`` nor ``openssl`` is available.
|
|
143
|
+
"""
|
|
144
|
+
if cert and key:
|
|
145
|
+
return cert, key
|
|
146
|
+
|
|
147
|
+
tmp = Path(tempfile.mkdtemp(prefix='webdev-tls-'))
|
|
148
|
+
cert_path = tmp / 'cert.pem'
|
|
149
|
+
key_path = tmp / 'key.pem'
|
|
150
|
+
|
|
151
|
+
if _generate_with_cryptography(cert_path, key_path):
|
|
152
|
+
return str(cert_path), str(key_path)
|
|
153
|
+
if _generate_with_openssl(cert_path, key_path):
|
|
154
|
+
return str(cert_path), str(key_path)
|
|
155
|
+
|
|
156
|
+
raise TLSUnavailableError(
|
|
157
|
+
'Could not generate a self-signed certificate: install the '
|
|
158
|
+
"'cryptography' package or the 'openssl' binary, or pass --cert/--key."
|
|
159
|
+
)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: webdev
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Small, fast development web server with a modern dark directory index
|
|
5
|
+
Project-URL: Homepage, https://github.com/kuralabs/webdev
|
|
6
|
+
Project-URL: Repository, https://github.com/kuralabs/webdev
|
|
7
|
+
Project-URL: Issues, https://github.com/kuralabs/webdev/issues
|
|
8
|
+
Author-email: "KuraLabs S.R.L" <info@kuralabs.io>, Carlos Jenkins <carlos@jenkins.co.cr>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: dev,development,http,server,static,web
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: colorlog>=6.8
|
|
23
|
+
Requires-Dist: jinja2>=3.1
|
|
24
|
+
Requires-Dist: qrcode>=7.4
|
|
25
|
+
Requires-Dist: starlette>=0.37
|
|
26
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
27
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
28
|
+
Requires-Dist: watchfiles>=0.21
|
|
29
|
+
Description-Content-Type: text/x-rst
|
|
30
|
+
|
|
31
|
+
======================
|
|
32
|
+
Development Web Server
|
|
33
|
+
======================
|
|
34
|
+
|
|
35
|
+
About
|
|
36
|
+
=====
|
|
37
|
+
|
|
38
|
+
``webdev`` is a small, fast development web server. It serves any local
|
|
39
|
+
directory over HTTP (or HTTPS) with developer-friendly conveniences, and ships
|
|
40
|
+
a modern, dark-themed directory index.
|
|
41
|
+
|
|
42
|
+
It is built on `Starlette <https://www.starlette.io/>`_ and
|
|
43
|
+
`Uvicorn <https://www.uvicorn.org/>`_ (an ASGI stack), and requires Python 3.10+.
|
|
44
|
+
|
|
45
|
+
Features
|
|
46
|
+
========
|
|
47
|
+
|
|
48
|
+
- **Serve any directory** — passed as the first positional argument; defaults to
|
|
49
|
+
the current directory.
|
|
50
|
+
- **Graceful when the folder disappears** — responds with ``404`` instead of
|
|
51
|
+
crashing if the served directory is removed, and **recovers automatically**
|
|
52
|
+
the moment it is recreated. Paths are resolved against the live filesystem on
|
|
53
|
+
every request, which is great with build systems that perform clean/rebuild
|
|
54
|
+
actions.
|
|
55
|
+
- **Automatic port selection** — if the base port is busy, the next free port in
|
|
56
|
+
a range is used. Can be forced to a single port.
|
|
57
|
+
- **Serve a not-yet-existing folder** — ``--future`` assumes the folder will be
|
|
58
|
+
created later.
|
|
59
|
+
- **Modern dark directory index** — breadcrumbs, file-type icons, human-readable
|
|
60
|
+
sizes, modified dates and client-side sortable columns. Themable: drop a new
|
|
61
|
+
directory under ``webdev/templates/<theme>/``.
|
|
62
|
+
- **Live reload** — connected browsers refresh automatically on file changes
|
|
63
|
+
(via a WebSocket; a tiny auto-reconnecting snippet is injected into HTML).
|
|
64
|
+
- **No-cache headers** for always-fresh development content.
|
|
65
|
+
- **SPA fallback** — optionally serve ``index.html`` for unknown routes.
|
|
66
|
+
- **Gzip**, **CORS**, optional **HTTPS** (self-signed if no cert/key is given),
|
|
67
|
+
and a startup **QR code / LAN URL** for quick mobile testing.
|
|
68
|
+
- **Config file** — every option can be set via ``webdev.toml`` (or a
|
|
69
|
+
``[tool.webdev]`` table). Precedence: CLI flag > config file > default.
|
|
70
|
+
|
|
71
|
+
Installation
|
|
72
|
+
============
|
|
73
|
+
|
|
74
|
+
::
|
|
75
|
+
|
|
76
|
+
pip install webdev
|
|
77
|
+
|
|
78
|
+
Usage
|
|
79
|
+
=====
|
|
80
|
+
|
|
81
|
+
::
|
|
82
|
+
|
|
83
|
+
$ webdev --help
|
|
84
|
+
usage: webdev [-h] [-v] [--version] [-i IP] [-p PORT] [-f] [-r RANGE] [-u]
|
|
85
|
+
[--reload | --no-reload] [--cache] [--no-cache]
|
|
86
|
+
[--spa | --no-spa] [--gzip | --no-gzip] [--cors | --no-cors]
|
|
87
|
+
[--qr | --no-qr] [--color | --no-color]
|
|
88
|
+
[--https | --no-https] [--cert CERT]
|
|
89
|
+
[--key KEY] [--theme THEME] [--config CONFIG]
|
|
90
|
+
[path]
|
|
91
|
+
|
|
92
|
+
Examples::
|
|
93
|
+
|
|
94
|
+
# Serve the current directory on http://localhost:8080
|
|
95
|
+
webdev
|
|
96
|
+
|
|
97
|
+
# Serve ./public on a forced port, over HTTPS, without live reload
|
|
98
|
+
webdev -p 3000 -f --https --no-reload ./public
|
|
99
|
+
|
|
100
|
+
# Single-page app: fall back to index.html for client-side routes
|
|
101
|
+
webdev --spa ./dist
|
|
102
|
+
|
|
103
|
+
Options and config keys
|
|
104
|
+
-----------------------
|
|
105
|
+
|
|
106
|
+
Every option has both a CLI flag and a config-file key. Booleans accept
|
|
107
|
+
``--x`` / ``--no-x``.
|
|
108
|
+
|
|
109
|
+
===================== ======================== ================== ==========
|
|
110
|
+
Feature CLI flag Config key Default
|
|
111
|
+
===================== ======================== ================== ==========
|
|
112
|
+
Serve path positional ``path`` ``path`` CWD
|
|
113
|
+
Bind IP ``-i/--ip`` ``ip`` 0.0.0.0
|
|
114
|
+
Base port ``-p/--port`` ``port`` 8080
|
|
115
|
+
Force exact port ``-f/--force-port`` ``force_port`` false
|
|
116
|
+
Port range ``-r/--range`` ``range`` 20
|
|
117
|
+
Serve missing folder ``-u/--future`` ``future`` false
|
|
118
|
+
No-cache headers ``--no-cache/--cache`` ``no_cache`` enabled
|
|
119
|
+
Live reload ``--reload/--no-reload`` ``reload`` enabled
|
|
120
|
+
SPA fallback ``--spa/--no-spa`` ``spa`` disabled
|
|
121
|
+
Gzip ``--gzip/--no-gzip`` ``gzip`` enabled
|
|
122
|
+
CORS ``--cors/--no-cors`` ``cors`` disabled
|
|
123
|
+
QR / LAN URL ``--qr/--no-qr`` ``qr`` enabled
|
|
124
|
+
Colorized logs ``--color/--no-color`` ``color`` enabled
|
|
125
|
+
HTTPS/TLS ``--https/--no-https`` ``https`` disabled
|
|
126
|
+
TLS cert / key ``--cert`` / ``--key`` ``cert`` / ``key`` self-signed
|
|
127
|
+
Index theme ``--theme`` ``theme`` dark
|
|
128
|
+
===================== ======================== ================== ==========
|
|
129
|
+
|
|
130
|
+
Example ``webdev.toml``::
|
|
131
|
+
|
|
132
|
+
port = 3000
|
|
133
|
+
force_port = true
|
|
134
|
+
spa = true
|
|
135
|
+
cors = true
|
|
136
|
+
|
|
137
|
+
Design notes
|
|
138
|
+
============
|
|
139
|
+
|
|
140
|
+
- **Framework:** Starlette + Uvicorn was chosen over a full framework such as
|
|
141
|
+
Sanic because this is a static file server: Starlette provides exactly the
|
|
142
|
+
primitives needed (routing, static serving, WebSockets, middleware) with a
|
|
143
|
+
small surface, and Uvicorn handles fast binding, HTTP keep-alive, HTTPS and
|
|
144
|
+
graceful shutdown.
|
|
145
|
+
- **The legacy reload hack is gone.** The old server patched ``send_head`` to
|
|
146
|
+
``chdir`` back into the served folder; modern ``http.server`` captures the
|
|
147
|
+
working directory in the handler constructor, so that hook stopped running and
|
|
148
|
+
a removed folder crashed the request. ``webdev`` now resolves every request
|
|
149
|
+
against the live filesystem, so missing folders yield a real ``404`` and
|
|
150
|
+
recover automatically.
|
|
151
|
+
|
|
152
|
+
Development
|
|
153
|
+
===========
|
|
154
|
+
|
|
155
|
+
The development workflow is driven by `tox <https://tox.wiki/>`_ using
|
|
156
|
+
`uv <https://docs.astral.sh/uv/>`_ as the backend (via ``tox-uv``). ``uv`` is the
|
|
157
|
+
only tool you need preinstalled::
|
|
158
|
+
|
|
159
|
+
# Lint, format-check and type-check
|
|
160
|
+
tox -e style
|
|
161
|
+
|
|
162
|
+
# Run the test suite
|
|
163
|
+
tox -e test
|
|
164
|
+
|
|
165
|
+
# Build and verify the distribution
|
|
166
|
+
tox -e build
|
|
167
|
+
|
|
168
|
+
Each environment declares its own tooling dependencies in ``tox.ini`` so it
|
|
169
|
+
installs only what it needs.
|
|
170
|
+
|
|
171
|
+
Changelog
|
|
172
|
+
=========
|
|
173
|
+
|
|
174
|
+
2.0.0
|
|
175
|
+
-----
|
|
176
|
+
|
|
177
|
+
**Reimplemented** on a modern async stack (Starlette + Uvicorn), Python 3.10+.
|
|
178
|
+
|
|
179
|
+
**New**
|
|
180
|
+
|
|
181
|
+
- Modern dark, themable directory index (breadcrumbs, icons, sizes, dates,
|
|
182
|
+
sortable columns), rendered with Jinja2.
|
|
183
|
+
- Live reload over WebSocket.
|
|
184
|
+
- No-cache headers, SPA fallback, gzip, CORS, optional HTTPS (self-signed),
|
|
185
|
+
startup QR code / LAN URL.
|
|
186
|
+
- Colorized, level-aware log output (``--color/--no-color``, via colorlog).
|
|
187
|
+
- Config-file support (``webdev.toml`` / ``[tool.webdev]``); every option has a
|
|
188
|
+
CLI flag and a config key (precedence CLI > config > default).
|
|
189
|
+
- ``pyproject.toml`` packaging; tox + uv development workflow; Ruff, mypy and a
|
|
190
|
+
pytest suite.
|
|
191
|
+
|
|
192
|
+
**Fixed**
|
|
193
|
+
|
|
194
|
+
- The directory auto-recovery feature, which silently broke on modern Python
|
|
195
|
+
when ``http.server`` began capturing the working directory in the request
|
|
196
|
+
handler constructor. Path resolution is now per-request against the live
|
|
197
|
+
filesystem.
|
|
198
|
+
|
|
199
|
+
**Changed**
|
|
200
|
+
|
|
201
|
+
- Dropped Python 2 support.
|
|
202
|
+
|
|
203
|
+
1.1.0
|
|
204
|
+
-----
|
|
205
|
+
|
|
206
|
+
**New**
|
|
207
|
+
|
|
208
|
+
- Added support for Python 3 (issue #5).
|
|
209
|
+
- Added support for incremental automatic port selection (issue #2).
|
|
210
|
+
- Added the ``--future`` flag that allows the webserver to serve a non-existent
|
|
211
|
+
folder, assuming it will be created in the future (issue #3).
|
|
212
|
+
|
|
213
|
+
**Changed**
|
|
214
|
+
|
|
215
|
+
- Ctrl+C is now handled (issue #4).
|
|
216
|
+
|
|
217
|
+
1.0.0
|
|
218
|
+
-----
|
|
219
|
+
|
|
220
|
+
- Initial release.
|
|
221
|
+
|
|
222
|
+
License
|
|
223
|
+
=======
|
|
224
|
+
|
|
225
|
+
::
|
|
226
|
+
|
|
227
|
+
Copyright (C) 2015-2026 Carlos Jenkins <carlos@jenkins.co.cr>
|
|
228
|
+
|
|
229
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
230
|
+
you may not use this file except in compliance with the License.
|
|
231
|
+
You may obtain a copy of the License at
|
|
232
|
+
|
|
233
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
234
|
+
|
|
235
|
+
Unless required by applicable law or agreed to in writing,
|
|
236
|
+
software distributed under the License is distributed on an
|
|
237
|
+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
238
|
+
KIND, either express or implied. See the License for the
|
|
239
|
+
specific language governing permissions and limitations
|
|
240
|
+
under the License.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
webdev/__init__.py,sha256=OkpQaCR6ZuY8wFMFvnDBeaRAXPX_rWu4aqlbRkxoi_U,1064
|
|
2
|
+
webdev/__main__.py,sha256=RhmDml_ICjjb3Ny0LDjH2HaIN2X0jGtzjwXBqPvsbwA,945
|
|
3
|
+
webdev/cli.py,sha256=GtZejOzYo8F5R1e8Hw8M3hoAcVHOEZbiFJcTJloFbM4,10504
|
|
4
|
+
webdev/config.py,sha256=4n4GnJHGCNBUV6Jsg4uEH9KGQJxr-CO9UkqkjHVEra4,6537
|
|
5
|
+
webdev/index.py,sha256=ZTufM0jYVQYpTdloZC7fN7hWbZtEZcmeJgaK3D-_JTY,9582
|
|
6
|
+
webdev/livereload.py,sha256=zghIaJUAWAdSPyWpGYS6sXlrPjyzEi5S2H9exnCQKQc,5658
|
|
7
|
+
webdev/net.py,sha256=JuEqWyRgy5sKadn3a6BCVFuvjrjL_5ZbUmwhIKCbHLE,5059
|
|
8
|
+
webdev/run.py,sha256=2fc5sstncBMes7PhVmiEtx5B8RVMSMmF4ISDVTa4OaY,3688
|
|
9
|
+
webdev/server.py,sha256=O1gWM53MYFhCANlGD9D6N0L2ipm93sczwsxYfiieE3M,10918
|
|
10
|
+
webdev/static.py,sha256=rCYsR_l_d0L8_Z0ZSCoWtMayJyYqleqeqjK6-TYiqlE,6613
|
|
11
|
+
webdev/tls.py,sha256=kUtaKMjssNXnPIzk8pUsEQbbPjQ8UVtdkE2jepusnaA,5861
|
|
12
|
+
webdev/templates/dark/base.html.jinja2,sha256=GQ3V1ViLMxqLcY6J4pTHBp3z6acAltSscQS1HhLhJn0,441
|
|
13
|
+
webdev/templates/dark/index.html.jinja2,sha256=Daq_Qq2_eU8tT7MYIT66ifAdCd_atJxdcUKWyR2jdbY,2369
|
|
14
|
+
webdev/templates/dark/partials/sort.js.jinja2,sha256=UyZGARVjREPhp_ExgwuxvCO5xn8yz-1p0yinAeULcfU,1792
|
|
15
|
+
webdev/templates/dark/partials/styles.css.jinja2,sha256=LbJsa_ocA0ynzJHkwnwyjdLICFVUgJ27oM6MLpTO_o4,2821
|
|
16
|
+
webdev-2.0.0.dist-info/METADATA,sha256=wQWSQp2Mslk-uW4s7SzigbgB5oYEwXbrcgoeOKevCbE,8949
|
|
17
|
+
webdev-2.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
18
|
+
webdev-2.0.0.dist-info/entry_points.txt,sha256=578xTHeC_DyXogKwIi1QFsIFcRwApzz7odTdApHijvk,43
|
|
19
|
+
webdev-2.0.0.dist-info/licenses/LICENSE,sha256=xllut76FgcGL5zbIRvuRc7aezPbvlMUTWJPsVr2Sugg,11358
|
|
20
|
+
webdev-2.0.0.dist-info/RECORD,,
|