From 08ff9519c3c601660605c30624f9caa35dc36e75 Mon Sep 17 00:00:00 2001 From: Bryce Boe Date: Sun, 14 Jun 2026 21:20:04 -0700 Subject: [PATCH 1/5] Port to Python 3 and support Python 3.10 through 3.14 The module was Python 2 only (SocketServer/BaseHTTPServer/SimpleHTTPServer imports, str-based base64, the removed ssl.wrap_socket) and would not import on Python 3. Port it to http.server/socketserver, argparse, ssl.SSLContext, pathlib, and f-strings; adopt the codesorter + ruff pre-commit stack; and declare support for Python 3.10 through 3.14. --- .pre-commit-config.yaml | 11 ++ README.md | 4 + ext_http_server.py | 329 ++++++++++++++++++++-------------------- lint.sh | 24 --- ruff.toml | 35 +++++ setup.py | 66 +++++--- 6 files changed, 257 insertions(+), 212 deletions(-) create mode 100644 .pre-commit-config.yaml mode change 100644 => 100755 ext_http_server.py delete mode 100755 lint.sh create mode 100644 ruff.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..23599ed --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: + - hooks: + - id: codesorter + repo: https://github.com/praw-dev/CodeSorter + rev: 8aa6144b41e0f789124b2ca377d246ffd1fbb317 # frozen: v0.2.7 + - hooks: + - args: [--fix] + id: ruff-check + - id: ruff-format + repo: https://github.com/astral-sh/ruff-pre-commit + rev: 3b3f7c3f57fe9925356faf5fe6230835138be230 # frozen: v0.15.17 diff --git a/README.md b/README.md index 67f1d96..24f8b6e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +### Requirements + +`ext_http_server` supports Python 3.10 through 3.14. + ### Installation pip install ext_http_server diff --git a/ext_http_server.py b/ext_http_server.py old mode 100644 new mode 100755 index 40a8d0c..78404cf --- a/ext_http_server.py +++ b/ext_http_server.py @@ -1,115 +1,55 @@ #!/usr/bin/env python """A small set of improvements upon the Simple and BaseHTTPServers.""" -import SocketServer +import argparse import base64 +import errno import os -import socket +import socketserver import ssl import sys import threading import time -from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler -from SimpleHTTPServer import SimpleHTTPRequestHandler -from optparse import OptionParser +from http.server import BaseHTTPRequestHandler, HTTPServer, SimpleHTTPRequestHandler +from pathlib import Path +from typing import ClassVar from warnings import warn - -__version__ = '0.2' - - -# -# Helpers -# -class RateLimitWriter(object): - """A class that rate limits writing to associated file streams - - This method only supports threading and not forking (multiprocessing). - """ - INTERVAL_LEN = .125 - block_size = 16384 - lock = threading.Lock() - block_start = None - block_sent = 0 - - @classmethod - def bytes_to_write(cls, desired): - """Determine how many bytes to write and sleep when over the limit.""" - to_send = 0 - while not to_send: - cls.lock.acquire() - now = time.time() - if not cls.block_start: - # First data of block, send it all - cls.block_start = now - to_send = min(desired, cls.block_size) - cls.block_sent = to_send - elif cls.block_sent < cls.block_size: - # Haven't sent a complete block, send remainder - to_send = min(desired, cls.block_size - cls.block_sent) - cls.block_sent += to_send - else: - # A complete block has been sent, sleep if necessary - sleep_time = cls.INTERVAL_LEN - (now - cls.block_start) - if sleep_time > 0: - time.sleep(sleep_time) - cls.block_start = cls.block_sent = None - cls.block_sent = 0 - cls.lock.release() - return to_send - - @classmethod - def set_rate_limit(cls, limit): - """Set the rate limit in kilobytes per second.""" - cls.block_size = int(1024 * limit * cls.INTERVAL_LEN) - - def __init__(self, to_wrap): - """Store the output stream we are wrapping.""" - self.wrapped = to_wrap - - def __getattr__(self, attr): - """Redirect all function calls through the wrapped output stream.""" - return getattr(self.wrapped, attr) - - def write(self, message): - """Perform a throttled write to the wrapped output stream.""" - while message: - to_send = RateLimitWriter.bytes_to_write(len(message)) - self.wrapped.write(message[:to_send]) - message = message[to_send:] - - -# -# HTTPServer extensions -# -class SecureHTTPServer(HTTPServer, object): - """A HTTP Server object that support HTTPS""" - def __init__(self, address, handler, cert_file): - """Support TLS/SSL by wrapping the socket.""" - super(SecureHTTPServer, self).__init__(address, handler) - self.socket = ssl.wrap_socket(self.socket, certfile=cert_file) +__version__ = "1.0" # # BaseHTTPRequestHandler extensions # -class AuthHandler(BaseHTTPRequestHandler, object): - """A handler that supports basic HTTP authentication/authorization""" - message = 'Authentication required.' - realm = 'Something' - users = set() +class AuthHandler(BaseHTTPRequestHandler): + """A handler that supports basic HTTP authentication/authorization.""" + + message = "Authentication required." + realm = "Something" + users: ClassVar[set[str]] = set() @classmethod def add_user(cls, username, password): """Add a set of credentials.""" - cls.users.add(base64.b64encode('{0}:{1}'.format(username, password))) + token = base64.b64encode(f"{username}:{password}".encode()).decode() + cls.users.add(token) + + def do_GET(self): + """Call the parent's do_GET function if the user is authorized.""" + if self.handle_auth(): + super().do_GET() - def handle_auth(self, head=False): + def do_HEAD(self): + """Call the parent's do_HEAD function if the user is authorized.""" + if self.handle_auth(head=True): + super().do_HEAD() + + def handle_auth(self, *, head=False): """Output the authentication headers if the user is not valid.""" - auth = self.headers.getheader('Authorization') + auth = self.headers.get("Authorization") if auth: try: - _, encoded = auth.split(' ', 1) + _, encoded = auth.split(" ", 1) except ValueError: encoded = None # Verify the user @@ -117,60 +57,50 @@ def handle_auth(self, head=False): return True # Send authentication header information self.send_response(401) - self.send_header('WWW-Authenticate', - 'Basic realm="{0}"'.format(AuthHandler.realm)) - self.send_header('Content-Type', 'text/html') - self.send_header('Content-Length', len(AuthHandler.message)) + self.send_header("WWW-Authenticate", f'Basic realm="{AuthHandler.realm}"') + self.send_header("Content-Type", "text/html") + self.send_header("Content-Length", str(len(AuthHandler.message))) self.end_headers() if not head: - self.wfile.write(AuthHandler.message) + self.wfile.write(AuthHandler.message.encode()) return False - def do_GET(self): - """Call the parent's do_GET function if the user is authorized.""" - if self.handle_auth(): - super(AuthHandler, self).do_GET() - def do_HEAD(self): - """Call the parent's do_HEAD function if the user is authorized.""" - if self.handle_auth(head=True): - super(AuthHandler, self).do_HEAD() - - -class RangeHandler(SimpleHTTPRequestHandler, object): - """A handler that supports HTTP requests with the Range header +class RangeHandler(SimpleHTTPRequestHandler): + """A handler that supports HTTP requests with the Range header. The Range header allows for the resume download functionality. """ + def copyfile(self, source, outputfile): """Copy only the ranged part of the file when appropriate.""" if self.is_ranged: source.seek(self.range_begin) - super(RangeHandler, self).copyfile(source, outputfile) + super().copyfile(source, outputfile) def do_GET(self): """Set is_ranged flag if a valid Range header is sent.""" self.handle_range() - super(RangeHandler, self).do_GET() + super().do_GET() def do_HEAD(self): """Set is_ranged flag if a valid Range header is sent.""" self.handle_range() - super(RangeHandler, self).do_HEAD() + super().do_HEAD() def handle_range(self): """Parse the Range header if it exists.""" self.is_ranged = False - if 'range' in self.headers: + if "range" in self.headers: try: - range_unit, other = self.headers['range'].split('=', 1) - if range_unit == 'bytes': - if ',' in other: # Handle only a single range - warn('Multiple ranges are not supported.') + range_unit, other = self.headers["range"].split("=", 1) + if range_unit == "bytes": + if "," in other: # Handle only a single range + warn("Multiple ranges are not supported.", stacklevel=2) return - begin, end = other.split('-', 1) + begin, end = other.split("-", 1) if end: - warn('Shortened ranges are not supported.') + warn("Shortened ranges are not supported.", stacklevel=2) return self.range_begin = int(begin) if begin else 0 self.range_end = None @@ -180,41 +110,38 @@ def handle_range(self): def send_header(self, key, value): """Modify Content-Length and add Content-Range when ranged.""" - if key == 'Content-Length' and self.is_ranged: + if key == "Content-Length" and self.is_ranged: length = int(value) - if self.range_end is None: - end = length - 1 - else: - end = min(self.range_end, length - 1) + end = length - 1 if self.range_end is None else min(self.range_end, length - 1) value = str(1 + end - self.range_begin) - self.send_header('Content-Range', 'bytes {0}-{1}/{2}' - .format(self.range_begin, end, length)) - super(RangeHandler, self).send_header(key, value) + self.send_header("Content-Range", f"bytes {self.range_begin}-{end}/{length}") + super().send_header(key, value) - def send_response(self, status, *args, **kwargs): + def send_response(self, code, message=None): """Send 206 status for ranged responses.""" - if self.is_ranged and status == 200: - status = 206 - super(RangeHandler, self).send_response(status, *args, **kwargs) + if self.is_ranged and code == 200: + code = 206 + super().send_response(code, message) def setup(self): """Set HTTP/1.1 as Range is supported only on HTTP/1.1.""" - super(RangeHandler, self).setup() - self.protocol_version = 'HTTP/1.1' + super().setup() + self.protocol_version = "HTTP/1.1" self.is_ranged = False -class RateLimitHandler(BaseHTTPRequestHandler, object): - """A hander that supports rate limiting from server to client. +class RateLimitHandler(BaseHTTPRequestHandler): + """A handler that supports rate limiting from server to client. - This handler will not properly rate limit if a ForkinMixIn is used in the + This handler will not properly rate limit if a ForkingMixIn is used in the HTTPServer object. However, it works great in combination with the ThreadingMixIn. """ + def handle(self): - """Setup rate limiting on the outgoing connection.""" + """Set up rate limiting on the outgoing connection.""" self.wfile = RateLimitWriter(self.wfile) - super(RateLimitHandler, self).handle() + super().handle() # @@ -224,63 +151,139 @@ class MyHandler(AuthHandler, RangeHandler, RateLimitHandler): """A handler that supports auth, download resuming, and throttling.""" -class MyServer(SocketServer.ThreadingMixIn, SecureHTTPServer): - """A threaded SecureHTTPServer with basic error filtering""" +# +# Helpers +# +class RateLimitWriter: + """A class that rate limits writing to associated file streams. + + This method only supports threading and not forking (multiprocessing). + """ + + INTERVAL_LEN = 0.125 + block_sent = 0 + block_size = 16384 + block_start = None + lock: ClassVar = threading.Lock() + + @classmethod + def bytes_to_write(cls, desired): + """Determine how many bytes to write and sleep when over the limit.""" + to_send = 0 + while not to_send: + with cls.lock: + now = time.time() + if not cls.block_start: + # First data of block, send it all + cls.block_start = now + to_send = min(desired, cls.block_size) + cls.block_sent = to_send + elif cls.block_sent < cls.block_size: + # Haven't sent a complete block, send remainder + to_send = min(desired, cls.block_size - cls.block_sent) + cls.block_sent += to_send + else: + # A complete block has been sent, sleep if necessary + sleep_time = cls.INTERVAL_LEN - (now - cls.block_start) + if sleep_time > 0: + time.sleep(sleep_time) + cls.block_start = None + cls.block_sent = 0 + return to_send + + @classmethod + def set_rate_limit(cls, limit): + """Set the rate limit in kilobytes per second.""" + cls.block_size = int(1024 * limit * cls.INTERVAL_LEN) + + def __getattr__(self, attr): + """Redirect all function calls through the wrapped output stream.""" + return getattr(self.wrapped, attr) + + def __init__(self, to_wrap): + """Store the output stream we are wrapping.""" + self.wrapped = to_wrap + + def write(self, message): + """Perform a throttled write to the wrapped output stream.""" + while message: + to_send = RateLimitWriter.bytes_to_write(len(message)) + self.wrapped.write(message[:to_send]) + message = message[to_send:] + + +# +# HTTPServer extensions +# +class SecureHTTPServer(HTTPServer): + """A HTTP Server object that supports HTTPS.""" + + def __init__(self, address, handler, cert_file): + """Support TLS/SSL by wrapping the socket.""" + super().__init__(address, handler) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(cert_file) + self.socket = context.wrap_socket(self.socket, server_side=True) + + +class MyServer(socketserver.ThreadingMixIn, SecureHTTPServer): + """A threaded SecureHTTPServer with basic error filtering.""" + def handle_error(self, request, client_address): """Disable tracebacks on connection close errors.""" - exc_type, exc_value, _ = sys.exc_info() - if exc_type is socket.error and exc_value[0] == 32: - print('{0} closed connection'.format(client_address)) - elif exc_type is ssl.SSLError and exc_value.errno == 1: - print('{0} SSL Error: bad write retry'.format(client_address)) + _, exc_value, _ = sys.exc_info() + if isinstance(exc_value, OSError) and exc_value.errno == errno.EPIPE: + print(f"{client_address} closed connection") + elif isinstance(exc_value, ssl.SSLError) and exc_value.errno == 1: + print(f"{client_address} SSL Error: bad write retry") else: - super(MyServer, self).handle_error(request, client_address) + super().handle_error(request, client_address) def main(): """Run a secure threaded server with auth resume and rate limit support.""" - parser = OptionParser(version='%prog {0}'.format(__version__)) - parser.add_option('-p', '--port', type='int', default='8000') - parser.add_option('-c', '--cert', help='The TLS/SSL certificate file') - parser.add_option('-d', '--directory', help='The directory to serve') - parser.add_option('-r', '--ratelimit', help='The ratelimit in KBps', - type='int', default=128) - parser.add_option('-a', '--add-auth', help='Add user:password combination', - action='append') - options, _ = parser.parse_args() + parser = argparse.ArgumentParser() + parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}") + parser.add_argument("-p", "--port", default=8000, type=int) + parser.add_argument("-c", "--cert", help="The TLS/SSL certificate file") + parser.add_argument("-d", "--directory", help="The directory to serve") + parser.add_argument("-r", "--ratelimit", default=128, help="The ratelimit in KBps", type=int) + parser.add_argument("-a", "--add-auth", action="append", help="Add user:password combination") + options = parser.parse_args() # Configure Services if not options.add_auth: - parser.error('At least one user must be added via --add-auth') + parser.error("At least one user must be added via --add-auth") for auth in options.add_auth: try: - username, password = auth.split(':', 1) + username, password = auth.split(":", 1) except ValueError: - parser.error('{0!r} is not a valid username:password'.format(auth)) + parser.error(f"{auth!r} is not a valid username:password") AuthHandler.add_user(username, password) RateLimitWriter.set_rate_limit(options.ratelimit) # Verify cert file if not options.cert: - parser.error('--cert must be provided') - cert_path = os.path.abspath(options.cert) - if not os.path.isfile(cert_path): - parser.error('Invalid cert file') + parser.error("--cert must be provided") + cert_path = Path(options.cert).resolve() + if not cert_path.is_file(): + parser.error("Invalid cert file") # Change into serving directory if options.directory: try: os.chdir(options.directory) except OSError: - parser.error('Invalid --directory') + parser.error("Invalid --directory") - server = MyServer(('', options.port), MyHandler, cert_path) - print('Server listening on port %d' % options.port) + server = MyServer(("", options.port), MyHandler, cert_path) + print(f"Server listening on port {options.port}") try: server.serve_forever() except KeyboardInterrupt: - print('\nGoodbye') + print("\nGoodbye") + return 0 -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main()) diff --git a/lint.sh b/lint.sh deleted file mode 100755 index a5fbb05..0000000 --- a/lint.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -module=ext_http_server.py - - -# pep8 -output=$(pep8 $module) -if [ -n "$output" ]; then - echo "---pep8---" - echo -e "$output" - exit 1 -fi - -# pylint -output=$(pylint $module 2> /dev/null) -if [ -n "$output" ]; then - echo "--pylint--" - echo -e "$output" -fi - -echo "---pyflakes---" -pyflakes $module - -exit 0 \ No newline at end of file diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..44a0e30 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,35 @@ +target-version = "py310" +line-length = 100 + +[lint] +ignore = [ + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line +] +select = [ + "A", # flake8-builtins + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D", # pydocstyle + "E", # pycodestyle errors + "EM", # flake8-errmsg + "EXE", # flake8-executable + "F", # pyflakes + "FA", # flake8-future-annotations + "FLY", # flynt + "G", # flake8-logging-format + "I", # isort + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PTH", # flake8-use-pathlib + "Q", # flake8-quotes + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # ruff-specific rules + "SIM", # flake8-simplify + "UP", # pyupgrade + "W", # pycodestyle warnings +] diff --git a/setup.py b/setup.py index 424dd73..eeb64f4 100644 --- a/setup.py +++ b/setup.py @@ -1,30 +1,46 @@ -import os +"""Packaging configuration for ext_http_server.""" + import re +from pathlib import Path + from setuptools import setup -MODULE_NAME = 'ext_http_server' +HERE = Path(__file__).parent -README = open(os.path.join(os.path.dirname(__file__), 'README.md')).read() -VERSION = re.search("__version__ = '([^']+)'", - open('{0}.py'.format(MODULE_NAME)).read()).group(1) +MODULE_NAME = "ext_http_server" +README = (HERE / "README.md").read_text(encoding="utf-8") +VERSION = re.search( + r'__version__ = "([^"]+)"', + (HERE / f"{MODULE_NAME}.py").read_text(encoding="utf-8"), +).group(1) -setup(name=MODULE_NAME, - author='Bryce Boe', - author_email='bbzbryce@gmail.com', - classifiers=['Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - #'Programming Language :: Python :: 3' - ], - description=('An extended version of python\'s SimpleHTTPServer that ' - 'supports https, authentication, rate limiting, and ' - 'download resuming.'), - entry_points={'console_scripts': ['{0} = {0}:main'.format(MODULE_NAME)]}, - install_requires=[], - keywords=['http resume', 'http rate limit', 'http authentication'], - license='Simplified BSD License', - long_description=README, - py_modules=[MODULE_NAME], - url = 'https://github.com/bboe/extended_http_server', - version=VERSION) +setup( + author="Bryce Boe", + author_email="bbzbryce@gmail.com", + classifiers=[ + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + ], + description=( + "An extended version of python's SimpleHTTPServer that supports https, " + "authentication, rate limiting, and download resuming." + ), + entry_points={"console_scripts": [f"{MODULE_NAME} = {MODULE_NAME}:main"]}, + install_requires=[], + keywords=["http resume", "http rate limit", "http authentication"], + license="Simplified BSD License", + long_description=README, + name=MODULE_NAME, + py_modules=[MODULE_NAME], + python_requires=">=3.10", + url="https://github.com/bboe/extended_http_server", + version=VERSION, +) From 28018b8ca4822d2cedeb21166fa8d4fa8e4348cb Mon Sep 17 00:00:00 2001 From: Bryce Boe Date: Sun, 14 Jun 2026 21:20:04 -0700 Subject: [PATCH 2/5] Modernize packaging: single pyproject and tag-driven releases Replace setup.py, ruff.toml, and MANIFEST.in with one pyproject.toml on the hatchling backend. Derive the version from the git tag via hatch-vcs (read at runtime through importlib.metadata) and add a trusted-publishing PyPI workflow that runs on a published GitHub release, with SHA-pinned actions. Document installation with uv. --- .github/workflows/publish.yml | 24 ++++++++++ MANIFEST.in | 1 - README.md | 8 +++- ext_http_server.py | 7 ++- pyproject.toml | 83 +++++++++++++++++++++++++++++++++++ ruff.toml | 35 --------------- setup.py | 46 ------------------- uv.lock | 7 +++ 8 files changed, 126 insertions(+), 85 deletions(-) create mode 100644 .github/workflows/publish.yml delete mode 100644 MANIFEST.in mode change 100755 => 100644 ext_http_server.py create mode 100644 pyproject.toml delete mode 100644 ruff.toml delete mode 100644 setup.py create mode 100644 uv.lock diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..9f014f0 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,24 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +permissions: {} + +jobs: + publish: + name: Build and publish + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/ext_http_server/ + permissions: + id-token: write # trusted publishing to PyPI (no API token) + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 # full history + tags so hatch-vcs can derive the version + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + - run: uv build + - run: uv publish --trusted-publishing always diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 2da52e3..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include *.md *.txt diff --git a/README.md b/README.md index 24f8b6e..2d28db6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ ### Installation - pip install ext_http_server +Install the `ext_http_server` command with [uv](https://docs.astral.sh/uv/): + + uv tool install ext_http_server ### Generate a certificate @@ -19,6 +21,10 @@ to serve them up with a max outgoing throughput of 16KBps: ext_http_server --cert cert.pem -d /tmp/path/to/files -r16 -a foo:bar +Or run it once without installing: + + uvx ext_http_server --cert cert.pem -d /tmp/path/to/files -r16 -a foo:bar + By default, you will be able to access the webserver at [https://localhost:8000](https://localhost:8000). To authenticate, use the username `foo` and the password `bar` as indicated by the `-a foo:bar` diff --git a/ext_http_server.py b/ext_http_server.py old mode 100755 new mode 100644 index 78404cf..50e6140 --- a/ext_http_server.py +++ b/ext_http_server.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """A small set of improvements upon the Simple and BaseHTTPServers.""" import argparse @@ -11,11 +10,15 @@ import threading import time from http.server import BaseHTTPRequestHandler, HTTPServer, SimpleHTTPRequestHandler +from importlib.metadata import PackageNotFoundError, version from pathlib import Path from typing import ClassVar from warnings import warn -__version__ = "1.0" +try: + __version__ = version("ext_http_server") +except PackageNotFoundError: # running from a source checkout without an install + __version__ = "unknown" # diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7f3c73a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,83 @@ +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling", "hatch-vcs"] + + +[project] +authors = [ + { name = "Bryce Boe", email = "bbzbryce@gmail.com" }, +] +dynamic = ["version"] +classifiers = [ + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +description = "An extended version of Python's SimpleHTTPServer that supports https, authentication, rate limiting, and download resuming." +keywords = ["http authentication", "http rate limit", "http resume"] +license = "BSD-2-Clause" +license-files = ["LICENSE.txt"] +name = "ext_http_server" +readme = "README.md" +requires-python = ">=3.10" + + +[project.scripts] +ext_http_server = "ext_http_server:main" + + +[project.urls] +Homepage = "https://github.com/bboe/extended_http_server" + + +[tool.hatch.version] +source = "vcs" + + +[tool.hatch.build.targets.wheel] +include = ["ext_http_server.py"] + + +[tool.ruff] +line-length = 100 +target-version = "py310" + + +[tool.ruff.lint] +ignore = [ + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line +] +select = [ + "A", # flake8-builtins + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D", # pydocstyle + "E", # pycodestyle errors + "EM", # flake8-errmsg + "EXE", # flake8-executable + "F", # pyflakes + "FA", # flake8-future-annotations + "FLY", # flynt + "G", # flake8-logging-format + "I", # isort + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PTH", # flake8-use-pathlib + "Q", # flake8-quotes + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # ruff-specific rules + "SIM", # flake8-simplify + "UP", # pyupgrade + "W", # pycodestyle warnings +] diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index 44a0e30..0000000 --- a/ruff.toml +++ /dev/null @@ -1,35 +0,0 @@ -target-version = "py310" -line-length = 100 - -[lint] -ignore = [ - "D203", # 1 blank line required before class docstring - "D213", # Multi-line docstring summary should start at the second line -] -select = [ - "A", # flake8-builtins - "ARG", # flake8-unused-arguments - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "D", # pydocstyle - "E", # pycodestyle errors - "EM", # flake8-errmsg - "EXE", # flake8-executable - "F", # pyflakes - "FA", # flake8-future-annotations - "FLY", # flynt - "G", # flake8-logging-format - "I", # isort - "ISC", # flake8-implicit-str-concat - "N", # pep8-naming - "PGH", # pygrep-hooks - "PIE", # flake8-pie - "PTH", # flake8-use-pathlib - "Q", # flake8-quotes - "RET", # flake8-return - "RSE", # flake8-raise - "RUF", # ruff-specific rules - "SIM", # flake8-simplify - "UP", # pyupgrade - "W", # pycodestyle warnings -] diff --git a/setup.py b/setup.py deleted file mode 100644 index eeb64f4..0000000 --- a/setup.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Packaging configuration for ext_http_server.""" - -import re -from pathlib import Path - -from setuptools import setup - -HERE = Path(__file__).parent - -MODULE_NAME = "ext_http_server" -README = (HERE / "README.md").read_text(encoding="utf-8") -VERSION = re.search( - r'__version__ = "([^"]+)"', - (HERE / f"{MODULE_NAME}.py").read_text(encoding="utf-8"), -).group(1) - -setup( - author="Bryce Boe", - author_email="bbzbryce@gmail.com", - classifiers=[ - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - ], - description=( - "An extended version of python's SimpleHTTPServer that supports https, " - "authentication, rate limiting, and download resuming." - ), - entry_points={"console_scripts": [f"{MODULE_NAME} = {MODULE_NAME}:main"]}, - install_requires=[], - keywords=["http resume", "http rate limit", "http authentication"], - license="Simplified BSD License", - long_description=README, - name=MODULE_NAME, - py_modules=[MODULE_NAME], - python_requires=">=3.10", - url="https://github.com/bboe/extended_http_server", - version=VERSION, -) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..737ce7a --- /dev/null +++ b/uv.lock @@ -0,0 +1,7 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "ext-http-server" +source = { editable = "." } From 0833f1ad258d4d48709db4b13dd00032988d6934 Mon Sep 17 00:00:00 2001 From: Bryce Boe Date: Sun, 14 Jun 2026 21:20:04 -0700 Subject: [PATCH 3/5] Type-annotate fully and enable pyright strict and all ruff rules Add complete type annotations and turn on pyright strict (dev dependency group plus a pre-commit hook). Switch ruff to select = ["ALL"] with preview and resolve every finding -- http.HTTPStatus for status codes, a try-free handle_range, documented returns -- ignoring only four rules, each justified inline. --- .pre-commit-config.yaml | 8 +++ ext_http_server.py | 155 +++++++++++++++++++++++++--------------- pyproject.toml | 49 ++++++------- uv.lock | 41 +++++++++++ 4 files changed, 168 insertions(+), 85 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23599ed..20f6856 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,3 +9,11 @@ repos: - id: ruff-format repo: https://github.com/astral-sh/ruff-pre-commit rev: 3b3f7c3f57fe9925356faf5fe6230835138be230 # frozen: v0.15.17 + - hooks: + - entry: uv run --group dev pyright + id: pyright + language: system + name: pyright + pass_filenames: false + types: [python] + repo: local diff --git a/ext_http_server.py b/ext_http_server.py index 50e6140..bca2d42 100644 --- a/ext_http_server.py +++ b/ext_http_server.py @@ -1,20 +1,29 @@ """A small set of improvements upon the Simple and BaseHTTPServers.""" +from __future__ import annotations + import argparse import base64 import errno +import io import os import socketserver import ssl import sys import threading import time +from http import HTTPStatus from http.server import BaseHTTPRequestHandler, HTTPServer, SimpleHTTPRequestHandler from importlib.metadata import PackageNotFoundError, version from pathlib import Path -from typing import ClassVar +from typing import TYPE_CHECKING, Any, AnyStr, ClassVar, cast from warnings import warn +if TYPE_CHECKING: + from socket import socket + + from _typeshed import SupportsRead, SupportsWrite + try: __version__ = version("ext_http_server") except PackageNotFoundError: # running from a source checkout without an install @@ -24,7 +33,7 @@ # # BaseHTTPRequestHandler extensions # -class AuthHandler(BaseHTTPRequestHandler): +class AuthHandler(SimpleHTTPRequestHandler): """A handler that supports basic HTTP authentication/authorization.""" message = "Authentication required." @@ -32,23 +41,28 @@ class AuthHandler(BaseHTTPRequestHandler): users: ClassVar[set[str]] = set() @classmethod - def add_user(cls, username, password): + def add_user(cls, username: str, password: str) -> None: """Add a set of credentials.""" token = base64.b64encode(f"{username}:{password}".encode()).decode() cls.users.add(token) - def do_GET(self): + def do_GET(self) -> None: """Call the parent's do_GET function if the user is authorized.""" if self.handle_auth(): super().do_GET() - def do_HEAD(self): + def do_HEAD(self) -> None: """Call the parent's do_HEAD function if the user is authorized.""" if self.handle_auth(head=True): super().do_HEAD() - def handle_auth(self, *, head=False): - """Output the authentication headers if the user is not valid.""" + def handle_auth(self, *, head: bool = False) -> bool: + """Output the authentication headers if the user is not valid. + + Returns: + ``True`` when the request carries valid credentials, ``False`` otherwise. + + """ auth = self.headers.get("Authorization") if auth: try: @@ -59,7 +73,7 @@ def handle_auth(self, *, head=False): if encoded in AuthHandler.users: return True # Send authentication header information - self.send_response(401) + self.send_response(HTTPStatus.UNAUTHORIZED) self.send_header("WWW-Authenticate", f'Basic realm="{AuthHandler.realm}"') self.send_header("Content-Type", "text/html") self.send_header("Content-Length", str(len(AuthHandler.message))) @@ -75,65 +89,71 @@ class RangeHandler(SimpleHTTPRequestHandler): The Range header allows for the resume download functionality. """ - def copyfile(self, source, outputfile): + is_ranged: bool + range_begin: int + range_end: int | None + + def copyfile(self, source: SupportsRead[AnyStr], outputfile: SupportsWrite[AnyStr]) -> None: """Copy only the ranged part of the file when appropriate.""" - if self.is_ranged: + if self.is_ranged and isinstance(source, io.IOBase): source.seek(self.range_begin) super().copyfile(source, outputfile) - def do_GET(self): + def do_GET(self) -> None: """Set is_ranged flag if a valid Range header is sent.""" self.handle_range() super().do_GET() - def do_HEAD(self): + def do_HEAD(self) -> None: """Set is_ranged flag if a valid Range header is sent.""" self.handle_range() super().do_HEAD() - def handle_range(self): + def handle_range(self) -> None: """Parse the Range header if it exists.""" self.is_ranged = False - if "range" in self.headers: - try: - range_unit, other = self.headers["range"].split("=", 1) - if range_unit == "bytes": - if "," in other: # Handle only a single range - warn("Multiple ranges are not supported.", stacklevel=2) - return - begin, end = other.split("-", 1) - if end: - warn("Shortened ranges are not supported.", stacklevel=2) - return - self.range_begin = int(begin) if begin else 0 - self.range_end = None - self.is_ranged = True - except ValueError: - pass - - def send_header(self, key, value): + raw = self.headers.get("range") + if not raw or "=" not in raw: + return + range_unit, _, other = raw.partition("=") + if range_unit != "bytes" or "-" not in other: + return + if "," in other: # Handle only a single range + warn("Multiple ranges are not supported.", stacklevel=2) + return + begin, _, end = other.partition("-") + if end: + warn("Shortened ranges are not supported.", stacklevel=2) + return + if begin and not begin.isdigit(): + return + self.range_begin = int(begin) if begin else 0 + self.range_end = None + self.is_ranged = True + + def send_header(self, keyword: str, value: str) -> None: """Modify Content-Length and add Content-Range when ranged.""" - if key == "Content-Length" and self.is_ranged: + if keyword == "Content-Length" and self.is_ranged: length = int(value) end = length - 1 if self.range_end is None else min(self.range_end, length - 1) value = str(1 + end - self.range_begin) self.send_header("Content-Range", f"bytes {self.range_begin}-{end}/{length}") - super().send_header(key, value) + super().send_header(keyword, value) - def send_response(self, code, message=None): + def send_response(self, code: int, message: str | None = None) -> None: """Send 206 status for ranged responses.""" - if self.is_ranged and code == 200: - code = 206 + if self.is_ranged and code == HTTPStatus.OK: + code = HTTPStatus.PARTIAL_CONTENT super().send_response(code, message) - def setup(self): + def setup(self) -> None: """Set HTTP/1.1 as Range is supported only on HTTP/1.1.""" super().setup() self.protocol_version = "HTTP/1.1" self.is_ranged = False -class RateLimitHandler(BaseHTTPRequestHandler): +class RateLimitHandler(SimpleHTTPRequestHandler): """A handler that supports rate limiting from server to client. This handler will not properly rate limit if a ForkingMixIn is used in the @@ -141,9 +161,10 @@ class RateLimitHandler(BaseHTTPRequestHandler): ThreadingMixIn. """ - def handle(self): + def handle(self) -> None: """Set up rate limiting on the outgoing connection.""" - self.wfile = RateLimitWriter(self.wfile) + # RateLimitWriter is a transparent write-proxy, not a BufferedIOBase subclass. + self.wfile = cast("io.BufferedIOBase", RateLimitWriter(self.wfile)) super().handle() @@ -163,15 +184,22 @@ class RateLimitWriter: This method only supports threading and not forking (multiprocessing). """ - INTERVAL_LEN = 0.125 - block_sent = 0 - block_size = 16384 - block_start = None + INTERVAL_LEN: ClassVar[float] = 0.125 + block_sent: ClassVar[int] = 0 + block_size: ClassVar[int] = 16384 + block_start: ClassVar[float] = 0.0 lock: ClassVar = threading.Lock() + wrapped: io.BufferedIOBase + @classmethod - def bytes_to_write(cls, desired): - """Determine how many bytes to write and sleep when over the limit.""" + def bytes_to_write(cls, desired: int) -> int: + """Determine how many bytes to write and sleep when over the limit. + + Returns: + The number of bytes the caller may write now. + + """ to_send = 0 while not to_send: with cls.lock: @@ -190,24 +218,29 @@ def bytes_to_write(cls, desired): sleep_time = cls.INTERVAL_LEN - (now - cls.block_start) if sleep_time > 0: time.sleep(sleep_time) - cls.block_start = None + cls.block_start = 0.0 cls.block_sent = 0 return to_send @classmethod - def set_rate_limit(cls, limit): + def set_rate_limit(cls, limit: float) -> None: """Set the rate limit in kilobytes per second.""" cls.block_size = int(1024 * limit * cls.INTERVAL_LEN) - def __getattr__(self, attr): - """Redirect all function calls through the wrapped output stream.""" + def __getattr__(self, attr: str) -> Any: # noqa: ANN401 + """Redirect all attribute access to the wrapped output stream. + + Returns: + The corresponding attribute of the wrapped stream. + + """ return getattr(self.wrapped, attr) - def __init__(self, to_wrap): + def __init__(self, to_wrap: io.BufferedIOBase) -> None: """Store the output stream we are wrapping.""" self.wrapped = to_wrap - def write(self, message): + def write(self, message: bytes) -> None: """Perform a throttled write to the wrapped output stream.""" while message: to_send = RateLimitWriter.bytes_to_write(len(message)) @@ -221,7 +254,12 @@ def write(self, message): class SecureHTTPServer(HTTPServer): """A HTTP Server object that supports HTTPS.""" - def __init__(self, address, handler, cert_file): + def __init__( + self, + address: tuple[str, int], + handler: type[BaseHTTPRequestHandler], + cert_file: str | os.PathLike[str], + ) -> None: """Support TLS/SSL by wrapping the socket.""" super().__init__(address, handler) context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) @@ -232,7 +270,7 @@ def __init__(self, address, handler, cert_file): class MyServer(socketserver.ThreadingMixIn, SecureHTTPServer): """A threaded SecureHTTPServer with basic error filtering.""" - def handle_error(self, request, client_address): + def handle_error(self, request: socket | tuple[bytes, socket], client_address: Any) -> None: # noqa: ANN401 """Disable tracebacks on connection close errors.""" _, exc_value, _ = sys.exc_info() if isinstance(exc_value, OSError) and exc_value.errno == errno.EPIPE: @@ -243,8 +281,13 @@ def handle_error(self, request, client_address): super().handle_error(request, client_address) -def main(): - """Run a secure threaded server with auth resume and rate limit support.""" +def main() -> int: + """Run a secure threaded server with auth resume and rate limit support. + + Returns: + The process exit status. + + """ parser = argparse.ArgumentParser() parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}") parser.add_argument("-p", "--port", default=8000, type=int) diff --git a/pyproject.toml b/pyproject.toml index 7f3c73a..788868d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,16 @@ ext_http_server = "ext_http_server:main" Homepage = "https://github.com/bboe/extended_http_server" +[dependency-groups] +dev = ["pyright"] + + +[tool.pyright] +include = ["ext_http_server.py"] +pythonVersion = "3.10" +typeCheckingMode = "strict" + + [tool.hatch.version] source = "vcs" @@ -46,38 +56,19 @@ include = ["ext_http_server.py"] [tool.ruff] line-length = 100 +preview = true target-version = "py310" [tool.ruff.lint] +select = ["ALL"] ignore = [ - "D203", # 1 blank line required before class docstring - "D213", # Multi-line docstring summary should start at the second line -] -select = [ - "A", # flake8-builtins - "ARG", # flake8-unused-arguments - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "D", # pydocstyle - "E", # pycodestyle errors - "EM", # flake8-errmsg - "EXE", # flake8-executable - "F", # pyflakes - "FA", # flake8-future-annotations - "FLY", # flynt - "G", # flake8-logging-format - "I", # isort - "ISC", # flake8-implicit-str-concat - "N", # pep8-naming - "PGH", # pygrep-hooks - "PIE", # flake8-pie - "PTH", # flake8-use-pathlib - "Q", # flake8-quotes - "RET", # flake8-return - "RSE", # flake8-raise - "RUF", # ruff-specific rules - "SIM", # flake8-simplify - "UP", # pyupgrade - "W", # pycodestyle warnings + "CPY001", # file-level copyright notice; LICENSE.txt is the canonical copyright + "D203", # 1 blank line required before class docstring (conflicts with D211) + "D213", # multi-line docstring summary should start at the second line (conflicts with D212) + "T201", # `print` is the intended console output for this CLI server ] + + +[tool.ruff.lint.pydocstyle] +convention = "google" diff --git a/uv.lock b/uv.lock index 737ce7a..b673044 100644 --- a/uv.lock +++ b/uv.lock @@ -5,3 +5,44 @@ requires-python = ">=3.10" [[package]] name = "ext-http-server" source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pyright" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pyright" }] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.410" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/53/e4d8ea1391bd4355231be6f91bf239479aa0014260ed3fb5526eeb12a1f2/pyright-1.1.410.tar.gz", hash = "sha256:07a073b8ba6749826773c1269773efa11b93440d9a6aa60419d9a3172d6dc488", size = 4062013, upload-time = "2026-06-01T17:35:48.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/33/288b5868fa00846dacf249633719d747893e54aebd196b9968ac1878a5d3/pyright-1.1.410-py3-none-any.whl", hash = "sha256:5e961bed37cacf96b3f7cd7b1da39b350a9239aa2e69138d0e88f728cfaf296c", size = 6082448, upload-time = "2026-06-01T17:35:46.387Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From 23f82df3b95082c6fab676659b56e19bd96bc441 Mon Sep 17 00:00:00 2001 From: Bryce Boe Date: Sun, 14 Jun 2026 21:20:04 -0700 Subject: [PATCH 4/5] Add tests and CI, and tidy the repo Add a pytest suite (pytest + trustme dependency group): unit tests for handle_range parsing, RateLimitWriter throttling/proxying, and add_user, plus a real TLS integration test exercising auth + Range together. Add a CI workflow (pre-commit lint/type plus a pytest matrix over Python 3.10-3.14, SHA-pinned). Remove stale section-banner comments, flesh out .gitignore, and give the README a title, description, and a modern ECDSA P-256 certificate command. --- .github/workflows/ci.yml | 33 ++++ .gitignore | 8 +- README.md | 14 +- ext_http_server.py | 12 -- pyproject.toml | 19 +++ tests/test_ext_http_server.py | 161 ++++++++++++++++++ uv.lock | 307 ++++++++++++++++++++++++++++++++++ 7 files changed, 539 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 tests/test_ext_http_server.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b15cd68 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: {} + +jobs: + lint: + name: Lint and type-check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 # hatch-vcs needs tags to build the project for pyright + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + - run: uvx pre-commit run --all-files + + test: + name: Test on Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 # hatch-vcs needs tags to build the project + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + - run: uv run --python ${{ matrix.python-version }} --group test pytest diff --git a/.gitignore b/.gitignore index 811e4ae..d0ea065 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ +*.egg-info/ *.pyc *~ +.coverage +.pytest_cache/ +.ruff_cache/ +.venv/ +__pycache__/ _build/ build/ dist/ -*.egg-info/ +htmlcov/ diff --git a/README.md b/README.md index 2d28db6..6bf291e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +# ext_http_server + +An extended version of Python's `http.server` that turns a simple static file +server into one supporting HTTPS, HTTP Basic authentication, server-to-client +rate limiting, and resumable downloads via HTTP `Range` requests. + ### Requirements `ext_http_server` supports Python 3.10 through 3.14. @@ -10,9 +16,13 @@ Install the `ext_http_server` command with [uv](https://docs.astral.sh/uv/): ### Generate a certificate -Run the following to generate cert.pem: +Generate a self-signed certificate, writing the private key and certificate +together into `cert.pem` (the single file `--cert` expects). This uses a modern +ECDSA P-256 key, which every common TLS client supports (Ed25519 keys are +newer but are rejected by some clients, including the LibreSSL-based `curl` that +ships with macOS as of June 2026): - openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout cert.pem + openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -noenc -keyout cert.pem -out cert.pem -days 365 -subj "/CN=localhost" ### Running ext_http_server diff --git a/ext_http_server.py b/ext_http_server.py index bca2d42..0f54c13 100644 --- a/ext_http_server.py +++ b/ext_http_server.py @@ -30,9 +30,6 @@ __version__ = "unknown" -# -# BaseHTTPRequestHandler extensions -# class AuthHandler(SimpleHTTPRequestHandler): """A handler that supports basic HTTP authentication/authorization.""" @@ -168,16 +165,10 @@ def handle(self) -> None: super().handle() -# -# Combined classes for use with the main functionality -# class MyHandler(AuthHandler, RangeHandler, RateLimitHandler): """A handler that supports auth, download resuming, and throttling.""" -# -# Helpers -# class RateLimitWriter: """A class that rate limits writing to associated file streams. @@ -248,9 +239,6 @@ def write(self, message: bytes) -> None: message = message[to_send:] -# -# HTTPServer extensions -# class SecureHTTPServer(HTTPServer): """A HTTP Server object that supports HTTPS.""" diff --git a/pyproject.toml b/pyproject.toml index 788868d..bb416fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ Homepage = "https://github.com/bboe/extended_http_server" [dependency-groups] dev = ["pyright"] +test = ["pytest", "trustme"] [tool.pyright] @@ -72,3 +73,21 @@ ignore = [ [tool.ruff.lint.pydocstyle] convention = "google" + + +[tool.ruff.lint.per-file-ignores] +"tests/**" = [ + "ANN", # annotations are optional in tests + "D", # docstrings are optional in tests + "INP001", # the tests directory is intentionally not a package + "PLR2004", # literal values are clearer than named constants in assertions + "RUF076", # an autouse fixture is the right tool for resetting global state + "S101", # asserts are how tests assert + "S106", # test credentials are not real secrets + "SLF001", # tests exercise private internals directly +] + + +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] diff --git a/tests/test_ext_http_server.py b/tests/test_ext_http_server.py new file mode 100644 index 0000000..f5b0599 --- /dev/null +++ b/tests/test_ext_http_server.py @@ -0,0 +1,161 @@ +import base64 +import http.client +import io +import ssl +import threading +from collections.abc import Iterator +from email.message import Message + +import pytest +import trustme + +from ext_http_server import AuthHandler, MyHandler, MyServer, RangeHandler, RateLimitWriter + + +@pytest.fixture(autouse=True) +def _reset_global_state() -> Iterator[None]: + AuthHandler.users.clear() + RateLimitWriter.block_start = 0.0 + RateLimitWriter.block_sent = 0 + RateLimitWriter.block_size = 16384 + yield + AuthHandler.users.clear() + + +@pytest.fixture +def secure_server(tmp_path, monkeypatch) -> Iterator[tuple[int, ssl.SSLContext]]: + (tmp_path / "data.txt").write_text("0123456789ABCDEFGHIJ") + monkeypatch.chdir(tmp_path) + authority = trustme.CA() + server_cert = authority.issue_cert("127.0.0.1") + with server_cert.private_key_and_cert_chain_pem.tempfile() as cert_path: + AuthHandler.add_user("user", "pass") + server = MyServer(("127.0.0.1", 0), MyHandler, cert_path) + thread = threading.Thread(daemon=True, target=server.serve_forever) + thread.start() + client_context = ssl.create_default_context() + authority.configure_trust(client_context) + try: + yield server.server_address[1], client_context + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + + +def build_range_handler(range_value=None): + handler = object.__new__(RangeHandler) + headers = Message() + if range_value is not None: + headers["Range"] = range_value + handler.headers = headers + return handler + + +def request_file(port, client_context, *, authenticate=False, range_header=None): + connection = http.client.HTTPSConnection("127.0.0.1", port, context=client_context) + headers = {} + if authenticate: + headers["Authorization"] = "Basic " + base64.b64encode(b"user:pass").decode() + if range_header: + headers["Range"] = range_header + try: + connection.request("GET", "/data.txt", headers=headers) + response = connection.getresponse() + return response.status, response.read() + finally: + connection.close() + + +def test_add_user_stores_base64_token(): + AuthHandler.add_user("user", "pass") + assert base64.b64encode(b"user:pass").decode() in AuthHandler.users + + +def test_bytes_to_write_caps_to_remaining_block(): + RateLimitWriter.block_size = 100 + RateLimitWriter.block_start = 0.0 + RateLimitWriter.block_sent = 0 + assert RateLimitWriter.bytes_to_write(10) == 10 # first write, under the block size + assert RateLimitWriter.bytes_to_write(2) == 2 # still within the block + assert RateLimitWriter.bytes_to_write(1000) == 88 # capped to what remains of the block + + +def test_handle_range_absent(): + handler = build_range_handler() + handler.handle_range() + assert handler.is_ranged is False + + +def test_handle_range_multiple_warns(): + handler = build_range_handler("bytes=0-0,2-2") + with pytest.warns(UserWarning, match="Multiple ranges"): + handler.handle_range() + assert handler.is_ranged is False + + +@pytest.mark.parametrize("range_value", ["bytes=5-9", "bytes=-100"]) +def test_handle_range_shortened_warns(range_value): + handler = build_range_handler(range_value) + with pytest.warns(UserWarning, match="Shortened ranges"): + handler.handle_range() + assert handler.is_ranged is False + + +@pytest.mark.parametrize("range_value", ["bytes=5", "items=5-", "bytes=a-", "kbytes=0-"]) +def test_handle_range_unsupported_is_silent(range_value, recwarn): + handler = build_range_handler(range_value) + handler.handle_range() + assert handler.is_ranged is False + assert not recwarn.list + + +@pytest.mark.parametrize( + ("range_value", "expected_begin"), + [("bytes=5-", 5), ("bytes=0-", 0), ("bytes=12-", 12)], +) +def test_handle_range_valid(range_value, expected_begin): + handler = build_range_handler(range_value) + handler.handle_range() + assert handler.is_ranged is True + assert handler.range_begin == expected_begin + assert handler.range_end is None + + +def test_rate_limit_writer_delegates_unknown_attributes(): + sink = io.BytesIO() + writer = RateLimitWriter(sink) + writer.flush() # proxied to the wrapped stream via __getattr__ + assert writer.closed is sink.closed + + +def test_rate_limit_writer_proxies_all_bytes(): + RateLimitWriter.block_size = 10**9 # large enough to avoid throttling/sleeping + sink = io.BytesIO() + RateLimitWriter(sink).write(b"hello world") + assert sink.getvalue() == b"hello world" + + +def test_server_requires_authentication(secure_server): + port, context = secure_server + status, _ = request_file(port, context) + assert status == 401 + + +def test_server_serves_authenticated_request(secure_server): + port, context = secure_server + status, body = request_file(port, context, authenticate=True) + assert status == 200 + assert body == b"0123456789ABCDEFGHIJ" + + +def test_server_serves_range_request(secure_server): + port, context = secure_server + status, body = request_file(port, context, authenticate=True, range_header="bytes=5-") + assert status == 206 + assert body == b"56789ABCDEFGHIJ" + + +def test_set_rate_limit_computes_block_size(): + RateLimitWriter.set_rate_limit(128) + assert RateLimitWriter.block_size == int(1024 * 128 * RateLimitWriter.INTERVAL_LEN) diff --git a/uv.lock b/uv.lock index b673044..f8cb80c 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,166 @@ version = 1 revision = 3 requires-python = ">=3.10" +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + [[package]] name = "ext-http-server" source = { editable = "." } @@ -10,11 +170,37 @@ source = { editable = "." } dev = [ { name = "pyright" }, ] +test = [ + { name = "pytest" }, + { name = "trustme" }, +] [package.metadata] [package.metadata.requires-dev] dev = [{ name = "pyright" }] +test = [ + { name = "pytest" }, + { name = "trustme" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] [[package]] name = "nodeenv" @@ -25,6 +211,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pyright" version = "1.1.410" @@ -38,6 +260,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/33/288b5868fa00846dacf249633719d747893e54aebd196b9968ac1878a5d3/pyright-1.1.410-py3-none-any.whl", hash = "sha256:5e961bed37cacf96b3f7cd7b1da39b350a9239aa2e69138d0e88f728cfaf296c", size = 6082448, upload-time = "2026-06-01T17:35:46.387Z" }, ] +[[package]] +name = "pytest" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "trustme" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/c5/931476f4cf1cd9e736f32651005078061a50dc164a2569fb874e00eb2786/trustme-1.2.1.tar.gz", hash = "sha256:6528ba2bbc7f2db41f33825c8dd13e3e3eb9d334ba0f909713c8c3139f4ae47f", size = 26844, upload-time = "2025-01-02T01:55:32.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/f3/c34dbabf6da5eda56fe923226769d40e11806952cd7f46655dd06e10f018/trustme-1.2.1-py3-none-any.whl", hash = "sha256:d768e5fc57c86dfc5ec9365102e9b092541cd6954b35d8c1eea01a84f35a762a", size = 16530, upload-time = "2025-01-02T01:55:30.181Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" From 5d295abff56a77b98f82ae7fbb175ccca12aa43a Mon Sep 17 00:00:00 2001 From: Bryce Boe Date: Sun, 14 Jun 2026 21:32:13 -0700 Subject: [PATCH 5/5] Add additional pre-commit hooks Add docstrfmt, auto-walrus, toml-sort, and the pre-commit/pre-commit-hooks battery (large-file, shebang, TOML/YAML, end-of-file, line-ending, pytest test naming, no-commit-to-branch, and trailing-whitespace checks), and apply the resulting docstring formatting, walrus rewrite, and TOML normalization. Omit sort-simple-yaml, which only handles single-job workflows and corrupts this repo's multi-job CI workflow. --- .pre-commit-config.yaml | 36 +++++++++++++++++++++++++ ext_http_server.py | 9 ++++--- pyproject.toml | 60 +++++++++++++++++------------------------ 3 files changed, 65 insertions(+), 40 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 20f6856..ddfec89 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,50 @@ repos: + - hooks: + - id: docstrfmt + require_serial: true + repo: https://github.com/LilSpazJoekp/docstrfmt + rev: 8688ba6420d7b5ca95a8ba0edf8a9953babdc3da # frozen: v2.1.1 + - hooks: - id: codesorter repo: https://github.com/praw-dev/CodeSorter rev: 8aa6144b41e0f789124b2ca377d246ffd1fbb317 # frozen: v0.2.7 + + - hooks: + - id: auto-walrus + repo: https://github.com/MarcoGorelli/auto-walrus + rev: 1743edbed52f3d61886b59d98a27164a8af29c0d # frozen: 0.4.1 + - hooks: - args: [--fix] id: ruff-check - id: ruff-format repo: https://github.com/astral-sh/ruff-pre-commit rev: 3b3f7c3f57fe9925356faf5fe6230835138be230 # frozen: v0.15.17 + + - hooks: + - files: ^(.*\.toml)$ + id: toml-sort-fix + repo: https://github.com/pappasam/toml-sort + rev: 2970ae9bb7124fe5117a27e10c10d2da051ce05a # frozen: v0.24.4 + + - hooks: + - id: check-added-large-files + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - args: [--fix=lf] + id: mixed-line-ending + - args: [--pytest-test-first] + files: ^tests/.*\.py$ + id: name-tests-test + - id: no-commit-to-branch + - id: trailing-whitespace + repo: https://github.com/pre-commit/pre-commit-hooks + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 + - hooks: - entry: uv run --group dev pyright id: pyright diff --git a/ext_http_server.py b/ext_http_server.py index 0f54c13..58d1cdb 100644 --- a/ext_http_server.py +++ b/ext_http_server.py @@ -60,8 +60,7 @@ def handle_auth(self, *, head: bool = False) -> bool: ``True`` when the request carries valid credentials, ``False`` otherwise. """ - auth = self.headers.get("Authorization") - if auth: + if auth := self.headers.get("Authorization"): try: _, encoded = auth.split(" ", 1) except ValueError: @@ -84,6 +83,7 @@ class RangeHandler(SimpleHTTPRequestHandler): """A handler that supports HTTP requests with the Range header. The Range header allows for the resume download functionality. + """ is_ranged: bool @@ -154,8 +154,8 @@ class RateLimitHandler(SimpleHTTPRequestHandler): """A handler that supports rate limiting from server to client. This handler will not properly rate limit if a ForkingMixIn is used in the - HTTPServer object. However, it works great in combination with the - ThreadingMixIn. + HTTPServer object. However, it works great in combination with the ThreadingMixIn. + """ def handle(self) -> None: @@ -173,6 +173,7 @@ class RateLimitWriter: """A class that rate limits writing to associated file streams. This method only supports threading and not forking (multiprocessing). + """ INTERVAL_LEN: ClassVar[float] = 0.125 diff --git a/pyproject.toml b/pyproject.toml index bb416fc..da19053 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,22 +2,25 @@ build-backend = "hatchling.build" requires = ["hatchling", "hatch-vcs"] +[dependency-groups] +dev = ["pyright"] +test = ["pytest", "trustme"] [project] authors = [ - { name = "Bryce Boe", email = "bbzbryce@gmail.com" }, + {name = "Bryce Boe", email = "bbzbryce@gmail.com"} ] dynamic = ["version"] classifiers = [ - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14" ] description = "An extended version of Python's SimpleHTTPServer that supports https, authentication, rate limiting, and download resuming." keywords = ["http authentication", "http rate limit", "http resume"] @@ -27,54 +30,41 @@ name = "ext_http_server" readme = "README.md" requires-python = ">=3.10" - [project.scripts] ext_http_server = "ext_http_server:main" - [project.urls] Homepage = "https://github.com/bboe/extended_http_server" - -[dependency-groups] -dev = ["pyright"] -test = ["pytest", "trustme"] - - -[tool.pyright] +[tool.hatch.build.targets.wheel] include = ["ext_http_server.py"] -pythonVersion = "3.10" -typeCheckingMode = "strict" - [tool.hatch.version] source = "vcs" - -[tool.hatch.build.targets.wheel] +[tool.pyright] include = ["ext_http_server.py"] +pythonVersion = "3.10" +typeCheckingMode = "strict" +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] [tool.ruff] line-length = 100 preview = true target-version = "py310" - [tool.ruff.lint] select = ["ALL"] ignore = [ "CPY001", # file-level copyright notice; LICENSE.txt is the canonical copyright "D203", # 1 blank line required before class docstring (conflicts with D211) "D213", # multi-line docstring summary should start at the second line (conflicts with D212) - "T201", # `print` is the intended console output for this CLI server + "T201" # `print` is the intended console output for this CLI server ] - -[tool.ruff.lint.pydocstyle] -convention = "google" - - [tool.ruff.lint.per-file-ignores] "tests/**" = [ "ANN", # annotations are optional in tests @@ -84,10 +74,8 @@ convention = "google" "RUF076", # an autouse fixture is the right tool for resetting global state "S101", # asserts are how tests assert "S106", # test credentials are not real secrets - "SLF001", # tests exercise private internals directly + "SLF001" # tests exercise private internals directly ] - -[tool.pytest.ini_options] -pythonpath = ["."] -testpaths = ["tests"] +[tool.ruff.lint.pydocstyle] +convention = "google"