diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..0f6aef18 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,80 @@ +name: Docs + +on: + push: + branches: [ master ] + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'scripts/build_settings_doc.py' + - 'gunicorn/config.py' + - 'requirements_dev.txt' + - '.github/workflows/docs.yml' + pull_request: + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'scripts/build_settings_doc.py' + - 'gunicorn/config.py' + - 'requirements_dev.txt' + - '.github/workflows/docs.yml' + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + pip install -r requirements_dev.txt + + - name: Build documentation + run: mkdocs build + + - name: Upload site artifact + uses: actions/upload-artifact@v4 + with: + name: gunicorn-site + path: site + retention-days: 7 + + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + pip install -r requirements_dev.txt + + - name: Build documentation + run: mkdocs build + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: site + publish_branch: gh-pages + cname: gunicorn.org + commit_message: "docs: deploy ${{ github.sha }}" diff --git a/.gitignore b/.gitignore index 581094b7..74eecc70 100755 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ examples/frameworks/pylonstest/pylonstest.egg-info/ MANIFEST nohup.out setuptools-* +site/ +docs/site/ diff --git a/Makefile b/Makefile index 3641cd5a..2c7d8bc2 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,15 @@ test: coverage: venv/bin/python setup.py test --cov +docs: + mkdocs build + +docs-serve: + mkdocs serve + clean: @rm -rf .Python MANIFEST build dist venv* *.egg-info *.egg @find . -type f -name "*.py[co]" -delete @find . -type d -name "__pycache__" -delete -.PHONY: build clean coverage test +.PHONY: build clean coverage docs docs-serve test diff --git a/README.rst b/README.rst index ddb22b4d..f87cc4a6 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ Gunicorn --------- +======== .. image:: https://img.shields.io/pypi/v/gunicorn.svg?style=flat :alt: PyPI version @@ -13,60 +13,58 @@ Gunicorn :alt: Build Status :target: https://github.com/benoitc/gunicorn/actions/workflows/tox.yml -.. image:: https://github.com/benoitc/gunicorn/actions/workflows/lint.yml/badge.svg - :alt: Lint Status - :target: https://github.com/benoitc/gunicorn/actions/workflows/lint.yml - Gunicorn 'Green Unicorn' is a Python WSGI HTTP Server for UNIX. It's a pre-fork worker model ported from Ruby's Unicorn_ project. The Gunicorn server is broadly compatible with various web frameworks, simply implemented, light on server resource usage, and fairly speedy. -Feel free to join us in `#gunicorn`_ on `Libera.chat`_. +**New in v24**: Native ASGI support (beta) for async frameworks like FastAPI! + +Quick Start +----------- + +.. code-block:: bash + + pip install gunicorn + gunicorn myapp:app --workers 4 + +For ASGI applications (FastAPI, Starlette): + +.. code-block:: bash + + gunicorn myapp:app --worker-class asgi + +Features +-------- + +- WSGI support for Django, Flask, Pyramid, and any WSGI framework +- **ASGI support** (beta) for FastAPI, Starlette, Quart +- uWSGI binary protocol for nginx integration +- Multiple worker types: sync, gthread, gevent, eventlet, asgi +- Graceful worker process management +- Compatible with Python 3.12+ Documentation ------------- -The documentation is hosted at https://docs.gunicorn.org. +Full documentation at https://gunicorn.org -Installation ------------- +- `Quickstart `_ +- `Configuration `_ +- `Deployment `_ +- `Settings Reference `_ -Gunicorn requires **Python 3.x >= 3.10**. - -Install from PyPI:: - - $ pip install gunicorn - - -Usage ------ - -Basic usage:: - - $ gunicorn [OPTIONS] APP_MODULE - -Where ``APP_MODULE`` is of the pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``. The -module name can be a full dotted path. The variable name refers to a WSGI -callable that should be found in the specified module. - -Example with test app:: - - $ cd examples - $ gunicorn --workers=2 test:app - - -Contributing ------------- - -See `our complete contributor's guide `_ for more details. +Community +--------- +- Report bugs on `GitHub Issues `_ +- Chat in `#gunicorn`_ on `Libera.chat`_ +- See `CONTRIBUTING.md `_ for contribution guidelines License ------- -Gunicorn is released under the MIT License. See the LICENSE_ file for more -details. +Gunicorn is released under the MIT License. See the LICENSE_ file for details. .. _Unicorn: https://bogomips.org/unicorn/ .. _`#gunicorn`: https://web.libera.chat/?channels=#gunicorn diff --git a/benchmarks/baseline.json b/benchmarks/baseline.json new file mode 100644 index 00000000..aeea15dd --- /dev/null +++ b/benchmarks/baseline.json @@ -0,0 +1,8 @@ +{ + "gthread": { + "simple": {}, + "simple_high_concurrency": {}, + "slow_io": {}, + "large_response": {} + } +} \ No newline at end of file diff --git a/benchmarks/quick_bench.sh b/benchmarks/quick_bench.sh new file mode 100755 index 00000000..fb93f056 --- /dev/null +++ b/benchmarks/quick_bench.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Quick benchmark for gthread worker + +set -e + +cd "$(dirname "$0")" + +echo "Starting gunicorn with gthread worker..." +../.venv/bin/python -m gunicorn \ + --worker-class gthread \ + --workers 2 \ + --threads 4 \ + --worker-connections 1000 \ + --bind 127.0.0.1:8765 \ + --access-logfile /dev/null \ + --error-logfile /dev/null \ + --log-level warning \ + simple_app:application & + +GUNICORN_PID=$! +sleep 3 + +echo "" +echo "=== Benchmark: Simple requests (10000 requests, 100 concurrent) ===" +ab -n 10000 -c 100 -k http://127.0.0.1:8765/ 2>&1 | grep -E "(Requests per second|Time per request|Failed requests)" + +echo "" +echo "=== Benchmark: High concurrency (5000 requests, 500 concurrent) ===" +ab -n 5000 -c 500 -k http://127.0.0.1:8765/ 2>&1 | grep -E "(Requests per second|Time per request|Failed requests)" + +echo "" +echo "=== Benchmark: Large response (1000 requests, 50 concurrent) ===" +ab -n 1000 -c 50 -k http://127.0.0.1:8765/large 2>&1 | grep -E "(Requests per second|Time per request|Failed requests)" + +echo "" +echo "Stopping gunicorn..." +kill $GUNICORN_PID 2>/dev/null || true +wait $GUNICORN_PID 2>/dev/null || true + +echo "Done!" diff --git a/benchmarks/run_benchmark.py b/benchmarks/run_benchmark.py new file mode 100755 index 00000000..a5b662a1 --- /dev/null +++ b/benchmarks/run_benchmark.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +""" +Benchmark script for gunicorn gthread worker. + +This script runs various benchmarks against gunicorn and reports performance metrics. +Requires: gunicorn, requests (for warmup), and wrk or ab for load testing. +""" + +import argparse +import json +import os +import signal +import subprocess +import sys +import time +from pathlib import Path + + +BENCHMARK_DIR = Path(__file__).parent +APP_MODULE = "simple_app:application" + + +def check_dependencies(): + """Check if required tools are available.""" + # Check for wrk (preferred) or ab + for tool in ['wrk', 'ab']: + try: + subprocess.run([tool, '--version'], capture_output=True, check=False) + return tool + except FileNotFoundError: + continue + print("Error: Neither 'wrk' nor 'ab' found. Install one of them.") + print(" macOS: brew install wrk") + print(" Linux: apt-get install wrk (or apache2-utils for ab)") + sys.exit(1) + + +def start_gunicorn(worker_class, workers, threads, connections, bind, extra_args=None): + """Start gunicorn server and return the process.""" + cmd = [ + sys.executable, '-m', 'gunicorn', + '--worker-class', worker_class, + '--workers', str(workers), + '--threads', str(threads), + '--worker-connections', str(connections), + '--bind', bind, + '--access-logfile', '-', + '--error-logfile', '-', + '--log-level', 'warning', + APP_MODULE, + ] + if extra_args: + cmd.extend(extra_args) + + env = os.environ.copy() + env['PYTHONPATH'] = str(BENCHMARK_DIR.parent) + + proc = subprocess.Popen( + cmd, + cwd=BENCHMARK_DIR, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # Wait for server to be ready + time.sleep(2) + return proc + + +def stop_gunicorn(proc): + """Stop the gunicorn server.""" + proc.send_signal(signal.SIGTERM) + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + + +def run_wrk_benchmark(url, duration, threads, connections): + """Run wrk benchmark and return results.""" + cmd = [ + 'wrk', + '-t', str(threads), + '-c', str(connections), + '-d', f'{duration}s', + '--latency', + url, + ] + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + return parse_wrk_output(result.stdout) + + +def run_ab_benchmark(url, requests, concurrency): + """Run Apache Bench benchmark and return results.""" + cmd = [ + 'ab', + '-n', str(requests), + '-c', str(concurrency), + '-k', # keepalive + url, + ] + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + return parse_ab_output(result.stdout) + + +def parse_wrk_output(output): + """Parse wrk output to extract metrics.""" + metrics = {} + for line in output.split('\n'): + if 'Requests/sec' in line: + metrics['requests_per_sec'] = float(line.split(':')[1].strip()) + elif 'Transfer/sec' in line: + metrics['transfer_per_sec'] = line.split(':')[1].strip() + elif 'Latency' in line and 'Distribution' not in line: + parts = line.split() + if len(parts) >= 2: + metrics['latency_avg'] = parts[1] + elif '50%' in line: + metrics['latency_p50'] = line.split()[1] + elif '99%' in line: + metrics['latency_p99'] = line.split()[1] + return metrics + + +def parse_ab_output(output): + """Parse ab output to extract metrics.""" + metrics = {} + for line in output.split('\n'): + if 'Requests per second' in line: + metrics['requests_per_sec'] = float(line.split(':')[1].split()[0]) + elif 'Time per request' in line and 'mean' in line: + metrics['latency_avg'] = line.split(':')[1].strip() + elif 'Transfer rate' in line: + metrics['transfer_per_sec'] = line.split(':')[1].strip() + return metrics + + +def run_benchmark_suite(tool, bind_addr): + """Run a suite of benchmarks.""" + results = {} + + # Test configurations + configs = [ + {'name': 'simple', 'path': '/', 'connections': 100}, + {'name': 'simple_high_concurrency', 'path': '/', 'connections': 500}, + {'name': 'slow_io', 'path': '/slow', 'connections': 50}, + {'name': 'large_response', 'path': '/large', 'connections': 100}, + ] + + for config in configs: + url = f'http://{bind_addr}{config["path"]}' + print(f" Running {config['name']}...") + + if tool == 'wrk': + metrics = run_wrk_benchmark( + url, + duration=10, + threads=4, + connections=config['connections'], + ) + else: + metrics = run_ab_benchmark( + url, + requests=10000, + concurrency=config['connections'], + ) + + results[config['name']] = metrics + print(f" Requests/sec: {metrics.get('requests_per_sec', 'N/A')}") + + return results + + +def main(): + parser = argparse.ArgumentParser(description='Benchmark gunicorn gthread worker') + parser.add_argument('--workers', type=int, default=2, help='Number of workers') + parser.add_argument('--threads', type=int, default=4, help='Threads per worker') + parser.add_argument('--connections', type=int, default=1000, help='Worker connections') + parser.add_argument('--bind', default='127.0.0.1:8000', help='Bind address') + parser.add_argument('--compare', action='store_true', help='Compare sync vs gthread') + parser.add_argument('--output', help='Output JSON file for results') + args = parser.parse_args() + + tool = check_dependencies() + print(f"Using benchmark tool: {tool}") + + all_results = {} + + if args.compare: + # Compare sync and gthread workers + for worker_class in ['sync', 'gthread']: + print(f"\nBenchmarking {worker_class} worker...") + proc = start_gunicorn( + worker_class=worker_class, + workers=args.workers, + threads=args.threads, + connections=args.connections, + bind=args.bind, + ) + try: + all_results[worker_class] = run_benchmark_suite(tool, args.bind) + finally: + stop_gunicorn(proc) + else: + # Just benchmark gthread + print("\nBenchmarking gthread worker...") + proc = start_gunicorn( + worker_class='gthread', + workers=args.workers, + threads=args.threads, + connections=args.connections, + bind=args.bind, + ) + try: + all_results['gthread'] = run_benchmark_suite(tool, args.bind) + finally: + stop_gunicorn(proc) + + # Print summary + print("\n" + "=" * 60) + print("BENCHMARK SUMMARY") + print("=" * 60) + for worker, results in all_results.items(): + print(f"\n{worker.upper()} Worker:") + for test, metrics in results.items(): + rps = metrics.get('requests_per_sec', 'N/A') + print(f" {test}: {rps} req/s") + + if args.output: + with open(args.output, 'w') as f: + json.dump(all_results, f, indent=2) + print(f"\nResults saved to {args.output}") + + +if __name__ == '__main__': + main() diff --git a/benchmarks/simple_app.py b/benchmarks/simple_app.py new file mode 100644 index 00000000..982f589d --- /dev/null +++ b/benchmarks/simple_app.py @@ -0,0 +1,18 @@ +# Simple WSGI app for benchmarking + +def application(environ, start_response): + """Basic hello world response.""" + path = environ.get('PATH_INFO', '/') + + if path == '/large': + body = b'X' * 65536 # 64KB + else: + body = b'Hello, World!' + + status = '200 OK' + headers = [ + ('Content-Type', 'text/plain'), + ('Content-Length', str(len(body))), + ] + start_response(status, headers) + return [body] diff --git a/docs/README.rst b/docs/README.rst new file mode 100644 index 00000000..6ce90ba9 --- /dev/null +++ b/docs/README.rst @@ -0,0 +1,29 @@ +Generate Documentation +====================== + +Requirements +------------ + +Install the documentation dependencies with:: + + pip install -r requirements_dev.txt + +This provides MkDocs with the Material theme and supporting plugins. + + +Build static HTML +----------------- +:: + + mkdocs build + +The rendered site is emitted into the ``site/`` directory. + + +Preview locally +--------------- +:: + + mkdocs serve + +This serves the documentation at http://127.0.0.1:8000/ with live reload. diff --git a/docs/content/2010-news.md b/docs/content/2010-news.md new file mode 100644 index 00000000..19e2816c --- /dev/null +++ b/docs/content/2010-news.md @@ -0,0 +1,190 @@ + +# Changelog - 2010 + +## 0.12.0 / 2010-12-22 + +- Add support for logging configuration using a ini file. + It uses the standard Python logging's module Configuration + file format and allows anyone to use his custom file handler +- Add IPV6 support +- Add multidomain application example +- Improve gunicorn_django command when importing settings module + using DJANGO_SETTINGS_MODULE environment variable +- Send appropriate error status on http parsing +- Fix pidfile, set permissions so other user can read + it and use it. +- Fix temporary file leaking +- Fix setpgrp issue, can now be launched via ubuntu upstart +- Set the number of workers to zero on WINCH + +## 0.11.2 / 2010-10-30 + +* Add SERVER_SOFTWARE to the os.environ +* Add support for django settings environment variable +* Add support for logging configuration in Paster ini-files +* Improve arbiter notification in asynchronous workers +* Display the right error when a worker can't be used +* Fix Django support +* Fix HUP with Paster applications +* Fix readline in wsgi.input + +## 0.11.1 / 2010-09-02 + +* Implement max-requests feature to prevent memory leaks. +* Added 'worker_exit' server hook. +* Reseed the random number generator after fork(). +* Improve Eventlet worker. +* Fix Django command `run_gunicorn`. +* Fix the default proc name internal setting. +* Workaround to prevent Gevent worker to segfault on MacOSX. + +## 0.11.0 / 2010-08-12 + +* Improve dramatically performances of Gevent and Eventlet workers +* Optimize HTTP parsing +* Drop Server and Date headers in start_response when provided. +* Fix latency issue in async workers + +## 0.10.1 / 2010-08-06 + +* Improve gevent's workers. Add "egg:gunicorn#gevent_wsgi" worker using + `gevent.wsgi `_ and + "egg:gunicorn#gevent_pywsgi" worker using `gevent.pywsgi + `_ . + **"egg:gunicorn#gevent"** using our own HTTP parser is still here and + is **recommended** for normal uses. Use the "gevent.wsgi" parser if you + need really fast connections and don't need streaming, keepalive or ssl. +* Add pre/post request hooks +* Exit more quietly +* Fix gevent dns issue + +## 0.10.0 / 2010-07-08 + +* New HTTP parser. +* New HUP behaviour. Re-reads the configuration and then reloads all + worker processes without changing the master process id. Helpful for + code reloading and monitoring applications like supervisord and runit. +* Added a preload configuration parameter. By default, application code + is now loaded after a worker forks. This couple with the new HUP + handling can be used for dev servers to do hot code reloading. Using + the preload flag can help a bit in small memory VM's. +* Allow people to pass command line arguments to WSGI applications. See: + `examples/alt_spec.py + `_ +* Added an example gevent reloader configuration: + `examples/example_gevent_reloader.py + `_. +* New gevent worker "egg:gunicorn#gevent2", working with gevent.wsgi. +* Internal refactoring and various bug fixes. +* New documentation website. + +## 0.9.1 / 2010-05-26 + +* Support https via X-Forwarded-Protocol or X-Forwarded-Ssl headers +* Fix configuration +* Remove -d options which was used instead of -D for daemon. +* Fix umask in unix socket + +## 0.9.0 / 2010-05-24 + +* Added *when_ready* hook. Called just after the server is started +* Added *preload* setting. Load application code before the worker processes + are forked. +* Refactored Config +* Fix pidfile +* Fix QUIT/HUP in async workers +* Fix reexec +* Documentation improvements + +## 0.8.1 / 2010-04-29 + +* Fix builtins import in config +* Fix installation with pip +* Fix Tornado WSGI support +* Delay application loading until after processing all configuration + +## 0.8.0 / 2010-04-22 + +* Refactored Worker management for better async support. Now use the -k option + to set the type of request processing to use +* Added support for Tornado_ + +## 0.7.2 / 2010-04-15 + +* Added --spew option to help debugging (installs a system trace hook) +* Some fixes in async arbiters +* Fix a bug in start_response on error + +## 0.7.1 / 2010-04-01 + +* Fix bug when responses have no body. + +## 0.7.0 / 2010-03-26 + +* Added support for Eventlet_ and Gevent_ based workers. +* Added Websockets_ support +* Fix Chunked Encoding +* Fix SIGWINCH on OpenBSD_ +* Fix `PEP 333`_ compliance for the write callable. + +## 0.6.5 / 2010-03-11 + +* Fix pidfile handling +* Fix Exception Error + +## 0.6.4 / 2010-03-08 + +* Use cStringIO for performance when possible. +* Fix worker freeze when a remote connection closes unexpectedly. + +## 0.6.3 / 2010-03-07 + +* Make HTTP parsing faster. +* Various bug fixes + +## 0.6.2 / 2010-03-01 + +* Added support for chunked response. +* Added proc_name option to the config file. +* Improved the HTTP parser. It now uses buffers instead of strings to store + temporary data. +* Improved performance when sending responses. +* Workers are now murdered by age (the oldest is killed first). + +## 0.6.1 / 2010-02-24 + +* Added gunicorn config file support for Django admin command +* Fix gunicorn config file. -c was broken. +* Removed TTIN/TTOU from workers which blocked other signals. + +## 0.6.0 / 2010-02-22 + +* Added setproctitle support +* Change privilege switch behavior. We now work like NGINX, master keeps the + permissions, new uid/gid permissions are only set for workers. + +## 0.5.1 / 2010-02-22 + +* Fix umask +* Added Debian packaging + +## 0.5.0 / 2010-02-20 + +* Added `configuration file `_ handler. +* Added support for pre/post fork hooks +* Added support for before_exec hook +* Added support for unix sockets +* Added launch of workers processes under different user/group +* Added umask option +* Added SCRIPT_NAME support +* Better support of some exotic settings for Django projects +* Better support of Paste-compatible applications +* Some refactoring to make the code easier to hack +* Allow multiple keys in request and response headers + +.. _Tornado: http://www.tornadoweb.org/ +.. _`PEP 333`: https://www.python.org/dev/peps/pep-0333/ +.. _Eventlet: http://eventlet.net/ +.. _Gevent: http://www.gevent.org/ +.. _OpenBSD: https://www.openbsd.org/ +.. _Websockets: https://html.spec.whatwg.org/multipage/web-sockets.html diff --git a/docs/content/2011-news.md b/docs/content/2011-news.md new file mode 100644 index 00000000..87de3ef8 --- /dev/null +++ b/docs/content/2011-news.md @@ -0,0 +1,66 @@ + +# Changelog - 2011 + +## 0.13.4 / 2011-09-23 + +- fix util.closerange function used to prevent leaking fds on python 2.5 + (typo.md) + +## 0.13.3 / 2011-09-19 +- refactor gevent worker +- prevent leaking fds on reexec +- fix inverted request_time computation + +## 0.13.2 / 2011-09-17 + +- Add support for Tornado 2.0 in tornado worker +- Improve access logs: allows customisation of the log format & add + request time +- Logger module is now pluggable +- Improve graceful shutdown in Python versions >= 2.6 +- Fix post_request root arity for compatibility +- Fix sendfile support +- Fix Django reloading + +## 0.13.1 / 2011-08-22 + +- Fix unix socket. log argument was missing. + +## 0.13.0 / 2011-08-22 + +- Improve logging: allows file-reopening and add access log file + compatible with the `apache combined log format `_ +- Add the possibility to set custom SSL headers. X-Forwarded-Protocol + and X-Forwarded-SSL are still the default +- New `on_reload` hook to customize how gunicorn spawn new workers on + SIGHUP +- Handle projects with relative path in django_gunicorn command +- Preserve path parameters in PATH_INFO +- post_request hook now accepts the environ as argument. +- When stopping the arbiter, close the listener asap. +- Fix Django command `run_gunicorn` in settings reloading +- Fix Tornado_ worker exiting +- Fix the use of sendfile in wsgi.file_wrapper + + +## 0.12.2 / 2011-05-18 + +- Add wsgi.file_wrapper optimised for FreeBSD, Linux & MacOSX (use + sendfile if available) +- Fix django run_gunicorn command. Make sure we reload the application + code. +- Fix django localisation +- Compatible with gevent 0.14dev + +## 0.12.1 / 2011-03-23 + +- Add "on_starting" hook. This hook can be used to set anything before + the arbiter really start +- Support bdist_rpm in setup +- Improve content-length handling (pep 3333) +- Improve Django support +- Fix daemonizing (#142) +- Fix ipv6 handling + + +.. _Tornado: http://www.tornadoweb.org/ diff --git a/docs/content/2012-news.md b/docs/content/2012-news.md new file mode 100644 index 00000000..7d338046 --- /dev/null +++ b/docs/content/2012-news.md @@ -0,0 +1,117 @@ + +# Changelog - 2012 + +## 0.17.0 / 2012-12-25 + +- allows gunicorn to bind to multiple address +- add SSL support +- add syslog support +- add nworkers_changed hook +- add response arg for post_request hook +- parse command line with argparse (replace deprecated optparse) +- fix PWD detection in arbiter +- miscellaneous PEP8 fixes + +## 0.16.1 / 2012-11-19 + +- Fix packaging + +## 0.16.0 / 2012-11-19 + +- **Added support for Python 3.2 & 3.3** +- Expose --pythonpath command to all gunicorn commands +- Honor $PORT environment variable, useful for deployment on heroku +- Removed support for Python 2.5 +- Make sure we reopen the logs on the console +- Fix django settings module detection from path +- Reverted timeout for client socket. Fix issue on blocking issues. +- Fixed gevent worker + +## 0.15.0 / 2012-10-18 + +- new documentation site on http://docs.gunicorn.org +- new website on http://gunicorn.org +- add `haproxy PROXY protocol `_ support +- add ForwardedAllowIPS option: allows to filter Front-end's IPs + allowed to handle X-Forwarded-* headers. +- add callable hooks for paster config +- add x-forwarded-proto as secure scheme default (Heroku is using this) +- allows gunicorn to load a pre-compiled application +- support file reopening & reexec for all loggers +- initialize the logging config file with defaults. +- set timeout for client socket (slow client DoS). +- NoMoreData, ChunkMissingTerminator, InvalidChunkSize are now + IOError exceptions +- fix graceful shutdown in gevent +- fix limit request line check + +## 0.14.6 / 2012-07-26 + + +- fix gevent & subproces +- fix request line length check +- fix keepalive = 0 +- fix tornado worker + +## 0.14.5 / 2012-06-24 + +- fix logging during daemonisation + +## 0.14.4 / 2012-06-24 + +- new --graceful-timeout option +- fix multiple issues with request limit +- more fixes in django settings resolutions +- fix gevent.core import +- fix keepalive=0 in eventlet worker +- fix handle_error display with the unix worker +- fix tornado.wsgi.WSGIApplication calling error + +- **breaking change**: take the control on graceful reload back. + graceful can't be overridden anymore using the on_reload function. + +## 0.14.3 / 2012-05-15 + +- improvement: performance of http.body.Body.readline() +- improvement: log HTTP errors in access log like Apache +- improvement: display traceback when the worker fails to boot +- improvement: makes gunicorn work with gevent 1.0 +- examples: websocket example now supports hybi13 +- fix: reopen log files after initialization +- fix: websockets support +- fix: django1.4 support +- fix: only load the paster application 1 time + +## 0.14.2 / 2012-03-16 + +- add validate_class validator: allows to use a class or a method to + initialize the app during in-code configuration +- add support for max_requests in tornado worker +- add support for disabling x_forwarded_for_header in tornado worker +- gevent_wsgi is now an alias of gevent_pywsgi +- Fix gevent_pywsgi worker + +## 0.14.1 / 2012-03-02 + +- fixing source archive, reducing its size + +## 0.14.0 / 2012-02-27 + +- check if Request line is too large: You can now pass the parameter + ``--limit-request-line`` or set the ``limit_request_line`` in your + configuration file to set the max size of the request line in bytes. +- limit the number of headers fields and their size. Add + ``--limit-request-field`` and ``limit-request-field-size`` settings +- add ``p`` variable to the log access format to log pidfile +- add ``{HeaderName}o`` variable to the logo access format to log the + response header HeaderName +- request header is now logged with the variable ``{HeaderName}i`` in the + access log file +- improve error logging +- support logging.configFile +- support django 1.4 in both gunicorn_django & run_gunicorn command +- improve reload in django run_gunicorn command (should just work now) +- allows people to set the ``X-Forwarded-For`` header key and disable it by + setting an empty string. +- fix support of Tornado +- many other fixes. diff --git a/docs/content/2013-news.md b/docs/content/2013-news.md new file mode 100644 index 00000000..117ca7d1 --- /dev/null +++ b/docs/content/2013-news.md @@ -0,0 +1,83 @@ + +# Changelog - 2013 + +## 18.0 / 2013-08-26 + +- new: add ``-e/--env`` command line argument to pass an environment variables to + gunicorn +- new: add ``--chdir`` command line argument to specified directory + before apps loading. - new: add wsgi.file_wrapper support in async workers +- new: add ``--paste`` command line argument to set the paster config file +- deprecated: the command ``gunicorn_django`` is now deprecated. You should now + run your application with the WSGI interface installed with your project (see + https://docs.djangoproject.com/en/1.4/howto/deployment/wsgi/gunicorn/) for + more infos. +- deprecated: the command ``gunicorn_paste`` is deprecated. You now should use + the new ``--paste`` argument to set the configuration file of your paster + application. +- fix: Removes unmatched leading quote from the beginning of the default access + log format string +- fix: null timeout +- fix: gevent worker +- fix: don't reload the paster app when using pserve +- fix: after closing for error do not keep alive the connection +- fix: responses 1xx, 204 and 304 should not force the connection to be closed + +## 17.5 / 2013-07-03 + +- new: add signals documentation +- new: add post_worker_init hook for workers +- new: try to use gunicorn.conf.py in current folder as the default + config file. +- fix graceful timeout with the Eventlet worker +- fix: don't raise an error when closing the socket if already closed +- fix: fix --settings parameter for django application and try to find + the django settings when using the ``gunicorn`` command. +- fix: give the initial global_conf to paster application +- fix: fix 'Expect: 100-continue' support on Python 3 + +### New versioning: + +With this release, the versioning of Gunicorn is changing. Gunicorn is +stable since a long time and there is no point to release a "1.0" now. +It should have been done since a long time. 0.17 really meant it was the +17th stable version. From the beginning we have only 2 kind of +releases: + +major release: releases with major changes or huge features added +services releases: fixes and minor features added So from now we will +apply the following versioning ``.``. For example ``17.5`` is a +service release. + +## 0.17.4 / 2013-04-24 + +- fix unix socket address parsing + +## 0.17.3 / 2013-04-23 + +- add systemd sockets support +- add ``python -m gunicorn.app.wsgiapp`` support +- improve logger class inheritance +- exit when the config file isn't found +- add the -R option to enable stdio inheritance in daemon mode +- don't close file descriptors > 3 in daemon mode +- improve STDOUT/STDERR logging +- fix pythonpath option +- fix pidfile creation on Python 3 +- fix gevent worker exit +- fix ipv6 detection when the platform isn't supporting it + +## 0.17.2 / 2013-01-07 + +- optimize readline +- make imports errors more visible when loading an app or a logging + class +- fix tornado worker: don't pass ssl options if there are none +- fix PEP3333: accept only bytetrings in the response body +- fix support on CYGWIN platforms + +## 0.17.1 / 2013-01-05 + +- add syslog facility name setting +- fix ``--version`` command line argument +- fix wsgi url_scheme for https diff --git a/docs/content/2014-news.md b/docs/content/2014-news.md new file mode 100644 index 00000000..ed1937c2 --- /dev/null +++ b/docs/content/2014-news.md @@ -0,0 +1,202 @@ + +# Changelog - 2014 + +!!! note + Please see [news](news.md) for the latest changes. + + +## 19.1.1 / 2014-08-16 + +### Changes + +### Core + +- fix [Issue #835](https://github.com/benoitc/gunicorn/issues/835): display correct pid of already running instance +- fix [PR #833](https://github.com/benoitc/gunicorn/pull/833): fix `PyTest` class in setup.py. + + +### Logging + +- fix [Issue #838](https://github.com/benoitc/gunicorn/issues/838): statsd logger, send statsd timing metrics in milliseconds +- fix [Issue #839](https://github.com/benoitc/gunicorn/issues/839): statsd logger, allows for empty log message while pushing + metrics and restore worker number in DEBUG logs +- fix [Issue #850](https://github.com/benoitc/gunicorn/issues/850): add timezone to logging +- fix [Issue #853](https://github.com/benoitc/gunicorn/issues/853): Respect logger_class setting unless statsd is on + +### AioHttp worker + +- fix [Issue #830](https://github.com/benoitc/gunicorn/issues/830) make sure gaiohttp worker is shipped with gunicorn. + +## 19.1 / 2014-07-26 + +### Changes + +### Core + +- fix [Issue #785](https://github.com/benoitc/gunicorn/issues/785): handle binary type address given to a client socket address +- fix graceful shutdown. make sure QUIT and TERMS signals are switched everywhere. +- [Issue #799](https://github.com/benoitc/gunicorn/issues/799): fix support loading config from module +- [Issue #805](https://github.com/benoitc/gunicorn/issues/805): fix check for file-like objects +- fix [Issue #815](https://github.com/benoitc/gunicorn/issues/815): args validation in WSGIApplication.init +- fix [Issue #787](https://github.com/benoitc/gunicorn/issues/787): check if we load a pyc file or not. + + +### Tornado worker + +- fix [Issue #771](https://github.com/benoitc/gunicorn/issues/771): support tornado 4.0 +- fix [Issue #783](https://github.com/benoitc/gunicorn/issues/783): x_headers error. The x-forwarded-headers option has been removed + in `c4873681299212d6082cd9902740eef18c2f14f1 + `_. + The discussion is available on [PR #633](https://github.com/benoitc/gunicorn/pull/633). + + +### AioHttp worker + +- fix: fetch all body in input. fix [Issue #803](https://github.com/benoitc/gunicorn/issues/803) +- fix: don't install the worker if python < 3.3 +- fix [Issue #822](https://github.com/benoitc/gunicorn/issues/822): Support UNIX sockets in gaiohttp worker + + +### Async worker + +- fix [Issue #790](https://github.com/benoitc/gunicorn/issues/790): StopIteration shouldn't be caught at this level. + + +### Logging + +- add statsd logging handler fix [Issue #748](https://github.com/benoitc/gunicorn/issues/748) + + +### Paster + +- fix [Issue #809](https://github.com/benoitc/gunicorn/issues/809): Set global logging configuration from a Paste config. + + +### Extra + +- fix RuntimeError in gunicorn.reloader ([Issue #807](https://github.com/benoitc/gunicorn/issues/807)) + + +### Documentation + +- update faq: put a note on how `watch logs in the console + `_ + since many people asked for it. + + +## 19.0 / 2014-06-12 + +Gunicorn 19.0 is a major release with new features and fixes. This +version improve a lot the usage of Gunicorn with python 3 by adding `two +new workers `_ +to it: `gthread` a fully threaded async worker using futures and `gaiohttp` a +worker using asyncio. + + +### Breaking Changes + +### Switch QUIT and TERM signals + +With this change, when gunicorn receives a QUIT all the workers are +killed immediately and exit and TERM is used for the graceful shutdown. + +Note: the old behaviour was based on the NGINX but the new one is more +correct according the following doc: + +https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html + +also it is complying with the way the signals are sent by heroku: + +https://devcenter.heroku.com/articles/python-faq#what-constraints-exist-when-developing-applications-on-heroku + +### Deprecations + +`run_gunicorn`, `gunicorn_django` and `gunicorn_paster` are now +completely deprecated and will be removed in the next release. Use the +`gunicorn` command instead. + + +### Changes + +### core + +- add aiohttp worker named `gaiohttp` using asyncio. Full async worker + on python 3. +- fix HTTP-violating excess whitespace in write_error output +- fix: try to log what happened in the worker after a timeout, add a + `worker_abort` hook on SIGABRT signal. +- fix: save listener socket name in workers so we can handle buffered + keep-alive requests after the listener has closed. +- add on_exit hook called just before exiting gunicorn. +- add support for python 3.4 +- fix: do not swallow unexpected errors when reaping +- fix: remove incompatible SSL option with python 2.6 +- add new async gthread worker and `--threads` options allows to set multiple + threads to listen on connection +- deprecate `gunicorn_django` and `gunicorn_paster` +- switch QUIT and TERM signal +- reap workers in SIGCHLD handler +- add universal wheel support +- use `email.utils.formatdate` in gunicorn.util.http_date +- deprecate the `--debug` option +- fix: log exceptions that occur after response start … +- allows loading of applications from `.pyc` files (#693) +- fix: issue #691, raw_env config file parsing +- use a dynamic timeout to wait for the optimal time. (Reduce power + usage) +- fix python3 support when notifying the arbiter +- add: honor $WEB_CONCURRENCY environment variable. Useful for heroku + setups. +- add: include tz offset in access log +- add: include access logs in the syslog handler. +- add --reload option for code reloading +- add the capability to load `gunicorn.base.Application` without the loading of + the arguments of the command line. It allows you to [embed gunicorn in your own application](custom.md). +- improve: set wsgi.multithread to True for async workers +- fix logging: make sure to redirect wsgi.errors when needed +- add: syslog logging can now be done to a unix socket +- fix logging: don't try to redirect stdout/stderr to the logfile. +- fix logging: don't propagate log +- improve logging: file option can be overridden by the gunicorn options + `--error-logfile` and `--access-logfile` if they are given. +- fix: don't override SERVER_* by the Host header +- fix: handle_error +- add more option to configure SSL +- fix: sendfile with SSL +- add: worker_int callback (to react on SIGTERM) +- fix: don't depend on entry point for internal classes, now absolute + modules path can be given. +- fix: Error messages are now encoded in latin1 +- fix: request line length check +- improvement: proxy_allow_ips: Allow proxy protocol if "*" specified +- fix: run worker's `setup` method before setting num_workers +- fix: FileWrapper inherit from `object` now +- fix: Error messages are now encoded in latin1 +- fix: don't spam the console on SIGWINCH. +- fix: logging -don't stringify T and D logging atoms (#621) +- add support for the latest django version +- deprecate `run_gunicorn` django option +- fix: sys imported twice + + +### gevent worker + +- fix: make sure to stop all listeners +- fix: monkey patching is now done in the worker +- fix: "global name 'hub' is not defined" +- fix: reinit `hub` on old versions of gevent +- support gevent 1.0 +- fix: add subprocess in monkey patching +- fix: add support for multiple listener + + +### eventlet worker + +- fix: merge duplicate EventletWorker.init_process method (fixes #657) +- fix: missing errno import for eventlet sendfile patch +- fix: add support for multiple listener + + +### tornado worker + +- add graceful stop support diff --git a/docs/content/2015-news.md b/docs/content/2015-news.md new file mode 100644 index 00000000..0cec6d1f --- /dev/null +++ b/docs/content/2015-news.md @@ -0,0 +1,187 @@ + +# Changelog - 2015 + +!!! note + Please see [news](news.md) for the latest changes. + + +## 19.4.3 / 2015/12/30 + +- fix: don't check if a file is writable using os.stat with SELINUX ([Issue #1171](https://github.com/benoitc/gunicorn/issues/1171)) + +## 19.4.2 / 2015/12/29 + +### Core + +- improvement: handle HaltServer in manage_workers ([Issue #1095](https://github.com/benoitc/gunicorn/issues/1095)) +- fix: Do not rely on sendfile sending requested count ([Issue #1155](https://github.com/benoitc/gunicorn/issues/1155)) +- fix: claridy --no-sendfile default ([Issue #1156](https://github.com/benoitc/gunicorn/issues/1156)) +- fix: LoggingCatch sendfile failure from no file descriptor ([Issue #1160](https://github.com/benoitc/gunicorn/issues/1160)) + +### Logging + +- fix: Always send access log to syslog if syslog is on +- fix: check auth before trying to own a file ([Issue #1157](https://github.com/benoitc/gunicorn/issues/1157)) + + +### Documentation + +- fix: Fix Slowloris broken link. ([Issue #1142](https://github.com/benoitc/gunicorn/issues/1142)) +- Tweak markup in faq.rst + +### Testing + +- fix: gaiohttp test ([Issue #1164](https://github.com/benoitc/gunicorn/issues/1164)) + +## 19.4.1 / 2015/11/25 + +- fix tornado worker ([Issue #1154](https://github.com/benoitc/gunicorn/issues/1154)) + +## 19.4.0 / 2015/11/20 + +### Core + +- fix: make sure that a user is able to access to the logs after dropping a + privilege ([Issue #1116](https://github.com/benoitc/gunicorn/issues/1116)) +- improvement: inherit the `Exception` class where it needs to be ([Issue #997](https://github.com/benoitc/gunicorn/issues/997)) +- fix: make sure headers are always encoded as latin1 RFC 2616 ([Issue #1102](https://github.com/benoitc/gunicorn/issues/1102)) +- improvement: reduce arbiter noise ([Issue #1078](https://github.com/benoitc/gunicorn/issues/1078)) +- fix: don't close the unix socket when the worker exit ([Issue #1088](https://github.com/benoitc/gunicorn/issues/1088)) +- improvement: Make last logged worker count an explicit instance var ([Issue #1078](https://github.com/benoitc/gunicorn/issues/1078)) +- improvement: prefix config file with its type ([Issue #836](https://github.com/benoitc/gunicorn/issues/836)) +- improvement: pidfile handing ([Issue #1042](https://github.com/benoitc/gunicorn/issues/1042)) +- fix: catch OSError as well as ValueError on race condition ([Issue #1052](https://github.com/benoitc/gunicorn/issues/1052)) +- improve support of ipv6 by backporting urlparse.urlsplit from Python 2.7 to + Python 2.6. +- fix: raise InvalidRequestLine when the line contains malicious data + ([Issue #1023](https://github.com/benoitc/gunicorn/issues/1023)) +- fix: fix argument to disable sendfile +- fix: add gthread to the list of supported workers ([Issue #1011](https://github.com/benoitc/gunicorn/issues/1011)) +- improvement: retry socket binding up to five times upon EADDRNOTAVAIL + ([Issue #1004](https://github.com/benoitc/gunicorn/issues/1004)) +- **breaking change**: only honor headers that can be encoded in ascii to comply to + the RFC 7230 (See [Issue #1151](https://github.com/benoitc/gunicorn/issues/1151)). + +### Logging + +- add new parameters to access log ([Issue #1132](https://github.com/benoitc/gunicorn/issues/1132)) +- fix: make sure that files handles are correctly reopened on HUP + ([Issue #627](https://github.com/benoitc/gunicorn/issues/627)) +- include request URL in error message ([Issue #1071](https://github.com/benoitc/gunicorn/issues/1071)) +- get username in access logs ([Issue #1069](https://github.com/benoitc/gunicorn/issues/1069)) +- fix statsd logging support on Python 3 ([Issue #1010](https://github.com/benoitc/gunicorn/issues/1010)) + +### Testing + +- use last version of mock. +- many fixes in Travis CI support +- miscellaneous improvements in tests + +### Thread worker + +- fix: Fix self.nr usage in ThreadedWorker so that auto restart works as + expected ([Issue #1031](https://github.com/benoitc/gunicorn/issues/1031)) + +### Gevent worker + +- fix quit signal handling ([Issue #1128](https://github.com/benoitc/gunicorn/issues/1128)) +- add support for Python 3 ([Issue #1066](https://github.com/benoitc/gunicorn/issues/1066)) +- fix: make graceful shutdown thread-safe ([Issue #1032](https://github.com/benoitc/gunicorn/issues/1032)) + +### Tornado worker + +- fix ssl options ([Issue #1146](https://github.com/benoitc/gunicorn/issues/1146), [Issue #1135](https://github.com/benoitc/gunicorn/issues/1135)) +- don't check timeout when stopping gracefully ([Issue #1106](https://github.com/benoitc/gunicorn/issues/1106)) + +### AIOHttp worker + +- add SSL support ([Issue #1105](https://github.com/benoitc/gunicorn/issues/1105)) + +### Documentation + +- fix link to proc name setting ([Issue #1144](https://github.com/benoitc/gunicorn/issues/1144)) +- fix worker class documentation ([Issue #1141](https://github.com/benoitc/gunicorn/issues/1141), [Issue #1104](https://github.com/benoitc/gunicorn/issues/1104)) +- clarify graceful timeout documentation ([Issue #1137](https://github.com/benoitc/gunicorn/issues/1137)) +- don't duplicate NGINX config files examples ([Issue #1050](https://github.com/benoitc/gunicorn/issues/1050), [Issue #1048](https://github.com/benoitc/gunicorn/issues/1048)) +- add `web.py` framework example ([Issue #1117](https://github.com/benoitc/gunicorn/issues/1117)) +- update Debian/Ubuntu installations instructions ([Issue #1112](https://github.com/benoitc/gunicorn/issues/1112)) +- clarify `pythonpath` setting description ([Issue #1080](https://github.com/benoitc/gunicorn/issues/1080)) +- tweak some example for python3 +- clarify `sendfile` documentation +- miscellaneous typos in source code comments (thanks!) +- clarify why REMOTE_ADD may not be the user's IP address ([Issue #1037](https://github.com/benoitc/gunicorn/issues/1037)) + + +### Misc + +- fix: reloader should survive SyntaxError ([Issue #994](https://github.com/benoitc/gunicorn/issues/994)) +- fix: expose the reloader class to the worker. + + + +## 19.3.0 / 2015/03/06 + +### Core + +- fix: [Issue #978](https://github.com/benoitc/gunicorn/issues/978) make sure a listener is inheritable +- add `check_config` class method to workers +- fix: [Issue #983](https://github.com/benoitc/gunicorn/issues/983) fix select timeout in sync worker with multiple + connections +- allows workers to access to the reloader. close [Issue #984](https://github.com/benoitc/gunicorn/issues/984) +- raise TypeError instead of AssertionError + +### Logging + +- make Logger.loglevel a class attribute + +### Documentation + +- fix: [Issue #988](https://github.com/benoitc/gunicorn/issues/988) fix syntax errors in examples/gunicorn_rc + + +## 19.2.1 / 2015/02/4 + +### Logging + +- expose loglevel in the Logger class + +### AsyncIO worker (gaiohttp.md) + +- fix [Issue #977](https://github.com/benoitc/gunicorn/issues/977) fix initial crash + +### Documentation + +- document security mailing-list in the contributing page. + +## 19.2 / 2015/01/30 + +### Core + +- optimize the sync workers when listening on a single interface +- add `--sendfile` settings to enable/disable sendfile. fix [Issue #856](https://github.com/benoitc/gunicorn/issues/856) . +- add the selectors module to the code base. [Issue #886](https://github.com/benoitc/gunicorn/issues/886) +- add `--max-requests-jitter` setting to set the maximum jitter to add to the + max-requests setting. +- fix [Issue #899](https://github.com/benoitc/gunicorn/issues/899) propagate proxy_protocol_info to keep-alive requests +- fix [Issue #863](https://github.com/benoitc/gunicorn/issues/863) worker timeout: dynamic timeout has been removed +- fix: Avoid world writable file + +### Logging + +- fix [Issue #941](https://github.com/benoitc/gunicorn/issues/941) set logconfig default to paster more trivially +- add statsd-prefix config setting: set the prefix to use when emitting statsd + metrics +- [Issue #832](https://github.com/benoitc/gunicorn/issues/832) log to console by default + +### Thread Worker + +- fix [Issue #908](https://github.com/benoitc/gunicorn/issues/908) make sure the worker can continue to accept requests + +### Eventlet Worker + +- fix [Issue #867](https://github.com/benoitc/gunicorn/issues/867) Fix eventlet shutdown to actively shut down the workers. + +### Documentation + +Many improvements and fixes have been done, see the detailed changelog for +more information. diff --git a/docs/content/2016-news.md b/docs/content/2016-news.md new file mode 100644 index 00000000..299713fa --- /dev/null +++ b/docs/content/2016-news.md @@ -0,0 +1,79 @@ + +# Changelog - 2016 + +!!! note + Please see [news](news.md) for the latest changes + + +## 19.6.0 / 2016/05/21 + +### Core & Logging + +- improvement of the binary upgrade behaviour using USR2: remove file locking ([Issue #1270](https://github.com/benoitc/gunicorn/issues/1270)) +- add the ``--capture-output`` setting to capture stdout/stderr tot the log + file ([Issue #1271](https://github.com/benoitc/gunicorn/issues/1271)) +- Allow disabling ``sendfile()`` via the ``SENDFILE`` environment variable + ([Issue #1252](https://github.com/benoitc/gunicorn/issues/1252)) +- fix reload under pycharm ([Issue #1129](https://github.com/benoitc/gunicorn/issues/1129)) + +### Workers + +- fix: make sure to remove the signal from the worker pipe ([Issue #1269](https://github.com/benoitc/gunicorn/issues/1269)) +- fix: **gthread** worker, handle removed socket in the select loop + ([Issue #1258](https://github.com/benoitc/gunicorn/issues/1258)) + +## 19.5.0 / 2016/05/10 + +### Core + +- fix: Ensure response to HEAD request won't have message body +- fix: lock domain socket and remove on last arbiter exit ([Issue #1220](https://github.com/benoitc/gunicorn/issues/1220)) +- improvement: use EnvironmentError instead of socket.error ([Issue #939](https://github.com/benoitc/gunicorn/issues/939)) +- add: new ``FORWARDED_ALLOW_IPS`` environment variable ([Issue #1205](https://github.com/benoitc/gunicorn/issues/1205)) +- fix: infinite recursion when destroying sockets ([Issue #1219](https://github.com/benoitc/gunicorn/issues/1219)) +- fix: close sockets on shutdown ([Issue #922](https://github.com/benoitc/gunicorn/issues/922)) +- fix: clean up sys.exc_info calls to drop circular refs ([Issue #1228](https://github.com/benoitc/gunicorn/issues/1228)) +- fix: do post_worker_init after load_wsgi ([Issue #1248](https://github.com/benoitc/gunicorn/issues/1248)) + +### Workers + +- fix access logging in gaiohttp worker ([Issue #1193](https://github.com/benoitc/gunicorn/issues/1193)) +- eventlet: handle QUIT in a new coroutine ([Issue #1217](https://github.com/benoitc/gunicorn/issues/1217)) +- gevent: remove obsolete exception clauses in run ([Issue #1218](https://github.com/benoitc/gunicorn/issues/1218)) +- tornado: fix extra "Server" response header ([Issue #1246](https://github.com/benoitc/gunicorn/issues/1246)) +- fix: unblock the wait loop under python 3.5 in sync worker ([Issue #1256](https://github.com/benoitc/gunicorn/issues/1256)) + +### Logging + +- fix: log message for listener reloading ([Issue #1181](https://github.com/benoitc/gunicorn/issues/1181)) +- Let logging module handle traceback printing ([Issue #1201](https://github.com/benoitc/gunicorn/issues/1201)) +- improvement: Allow configuring logger_class with statsd_host ([Issue #1188](https://github.com/benoitc/gunicorn/issues/1188)) +- fix: traceback formatting ([Issue #1235](https://github.com/benoitc/gunicorn/issues/1235)) +- fix: print error logs on stderr and access logs on stdout ([Issue #1184](https://github.com/benoitc/gunicorn/issues/1184)) + + +### Documentation + +- Simplify installation instructions in gunicorn.org ([Issue #1072](https://github.com/benoitc/gunicorn/issues/1072)) +- Fix URL and default worker type in example_config ([Issue #1209](https://github.com/benoitc/gunicorn/issues/1209)) +- update django doc url to 1.8 lts ([Issue #1213](https://github.com/benoitc/gunicorn/issues/1213)) +- fix: miscellaneous wording corrections ([Issue #1216](https://github.com/benoitc/gunicorn/issues/1216)) +- Add PSF License Agreement of selectors.py to NOTICE (:issue: `1226`) +- document LOGGING overriding ([Issue #1051](https://github.com/benoitc/gunicorn/issues/1051)) +- put a note that error logs are only errors from Gunicorn ([Issue #1124](https://github.com/benoitc/gunicorn/issues/1124)) +- add a note about the requirements of the threads workers under python 2.x ([Issue #1200](https://github.com/benoitc/gunicorn/issues/1200)) +- add access_log_format to config example ([Issue #1251](https://github.com/benoitc/gunicorn/issues/1251)) + +### Tests + +- Use more pytest.raises() in test_http.py + + +## 19.4.5 / 2016/01/05 + +- fix: NameError fileno in gunicorn.http.wsgi ([Issue #1178](https://github.com/benoitc/gunicorn/issues/1178)) + +## 19.4.4 / 2016/01/04 + +- fix: check if a fileobject can be used with sendfile(2.md) ([Issue #1174](https://github.com/benoitc/gunicorn/issues/1174)) +- doc: be more descriptive in errorlog option ([Issue #1173](https://github.com/benoitc/gunicorn/issues/1173)) diff --git a/docs/content/2017-news.md b/docs/content/2017-news.md new file mode 100644 index 00000000..803f363f --- /dev/null +++ b/docs/content/2017-news.md @@ -0,0 +1,42 @@ + +# Changelog - 2017 + +!!! note + Please see [news](news.md) for the latest changes + + +## 19.7.1 / 2017/03/21 + +- fix: continue if SO_REUSEPORT seems to be available but fails ([Issue #1480](https://github.com/benoitc/gunicorn/issues/1480)) +- fix: support non-decimal values for the umask command line option ([Issue #1325](https://github.com/benoitc/gunicorn/issues/1325)) + +## 19.7.0 / 2017/03/01 + +- The previously deprecated ``gunicorn_django`` command has been removed. + Use the [gunicorn-cmd](run.md#gunicorn) command-line interface instead. +- The previously deprecated ``django_settings`` setting has been removed. + Use the [raw-env](reference/settings.md#raw_env) setting instead. +- The default value of [ssl-version](reference/settings.md#ssl_version) has been changed from + ``ssl.PROTOCOL_TLSv1`` to ``ssl.PROTOCOL_SSLv23``. +- fix: initialize the group access list when initgroups is set ([Issue #1297](https://github.com/benoitc/gunicorn/issues/1297)) +- add environment variables to gunicorn access log format ([Issue #1291](https://github.com/benoitc/gunicorn/issues/1291)) +- add --paste-global-conf option ([Issue #1304](https://github.com/benoitc/gunicorn/issues/1304)) +- fix: print access logs to STDOUT ([Issue #1184](https://github.com/benoitc/gunicorn/issues/1184)) +- remove upper limit on max header size config ([Issue #1313](https://github.com/benoitc/gunicorn/issues/1313)) +- fix: print original exception on AppImportError ([Issue #1334](https://github.com/benoitc/gunicorn/issues/1334)) +- use SO_REUSEPORT if available ([Issue #1344](https://github.com/benoitc/gunicorn/issues/1344)) +- `fix leak `_ of duplicate file descriptor for bound sockets. +- add --reload-engine option, support inotify and other backends ([Issue #1368](https://github.com/benoitc/gunicorn/issues/1368), [Issue #1459](https://github.com/benoitc/gunicorn/issues/1459)) +- fix: reject request with invalid HTTP versions +- add ``child_exit`` callback ([Issue #1394](https://github.com/benoitc/gunicorn/issues/1394)) +- add support for eventlets _AlreadyHandled object ([Issue #1406](https://github.com/benoitc/gunicorn/issues/1406)) +- format boot tracebacks properly with reloader ([Issue #1408](https://github.com/benoitc/gunicorn/issues/1408)) +- refactor socket activation and fd inheritance for better support of SystemD ([Issue #1310](https://github.com/benoitc/gunicorn/issues/1310)) +- fix: o fds are given by default in gunicorn ([Issue #1423](https://github.com/benoitc/gunicorn/issues/1423)) +- add ability to pass settings to GUNICORN_CMD_ARGS environment variable which helps in container world ([Issue #1385](https://github.com/benoitc/gunicorn/issues/1385)) +- fix: catch access denied to pid file ([Issue #1091](https://github.com/benoitc/gunicorn/issues/1091)) +- many additions and improvements to the documentation + +### Breaking Change + +- **Python 2.6.0** is the last supported version diff --git a/docs/content/2018-news.md b/docs/content/2018-news.md new file mode 100644 index 00000000..3c36e808 --- /dev/null +++ b/docs/content/2018-news.md @@ -0,0 +1,64 @@ + +# Changelog - 2018 + +!!! note + Please see [news](news.md) for the latest changes + + +## 19.9.0 / 2018/07/03 + +- fix: address a regression that prevented syslog support from working + ([Issue #1668](https://github.com/benoitc/gunicorn/issues/1668), [PR #1773](https://github.com/benoitc/gunicorn/pull/1773)) +- fix: correctly set `REMOTE_ADDR` on versions of Python 3 affected by + `Python Issue 30205 `_ + ([Issue #1755](https://github.com/benoitc/gunicorn/issues/1755), [PR #1796](https://github.com/benoitc/gunicorn/pull/1796)) +- fix: show zero response length correctly in access log ([PR #1787](https://github.com/benoitc/gunicorn/pull/1787)) +- fix: prevent raising `AttributeError` when ``--reload`` is not passed + in case of a `SyntaxError` raised from the WSGI application. + ([Issue #1805](https://github.com/benoitc/gunicorn/issues/1805), [PR #1806](https://github.com/benoitc/gunicorn/pull/1806)) +- The internal module ``gunicorn.workers.async`` was renamed to ``gunicorn.workers.base_async`` + since ``async`` is now a reserved word in Python 3.7. + ([PR #1527](https://github.com/benoitc/gunicorn/pull/1527)) + +## 19.8.1 / 2018/04/30 + +- fix: secure scheme headers when bound to a unix socket + ([Issue #1766](https://github.com/benoitc/gunicorn/issues/1766), [PR #1767](https://github.com/benoitc/gunicorn/pull/1767)) + +## 19.8.0 / 2018/04/28 + +- Eventlet 0.21.0 support ([Issue #1584](https://github.com/benoitc/gunicorn/issues/1584)) +- Tornado 5 support ([Issue #1728](https://github.com/benoitc/gunicorn/issues/1728), [PR #1752](https://github.com/benoitc/gunicorn/pull/1752)) +- support watching additional files with ``--reload-extra-file`` + ([PR #1527](https://github.com/benoitc/gunicorn/pull/1527)) +- support configuring logging with a dictionary with ``--logging-config-dict`` + ([Issue #1087](https://github.com/benoitc/gunicorn/issues/1087), [PR #1110](https://github.com/benoitc/gunicorn/pull/1110), [PR #1602](https://github.com/benoitc/gunicorn/pull/1602)) +- add support for the ``--config`` flag in the ``GUNICORN_CMD_ARGS`` environment + variable ([Issue #1576](https://github.com/benoitc/gunicorn/issues/1576), [PR #1581](https://github.com/benoitc/gunicorn/pull/1581)) +- disable ``SO_REUSEPORT`` by default and add the ``--reuse-port`` setting + ([Issue #1553](https://github.com/benoitc/gunicorn/issues/1553), [Issue #1603](https://github.com/benoitc/gunicorn/issues/1603), [PR #1669](https://github.com/benoitc/gunicorn/pull/1669)) +- fix: installing `inotify` on MacOS no longer breaks the reloader + ([Issue #1540](https://github.com/benoitc/gunicorn/issues/1540), [PR #1541](https://github.com/benoitc/gunicorn/pull/1541)) +- fix: do not throw ``TypeError`` when ``SO_REUSEPORT`` is not available + ([Issue #1501](https://github.com/benoitc/gunicorn/issues/1501), [PR #1491](https://github.com/benoitc/gunicorn/pull/1491)) +- fix: properly decode HTTP paths containing certain non-ASCII characters + ([Issue #1577](https://github.com/benoitc/gunicorn/issues/1577), [PR #1578](https://github.com/benoitc/gunicorn/pull/1578)) +- fix: remove whitespace when logging header values under gevent ([PR #1607](https://github.com/benoitc/gunicorn/pull/1607)) +- fix: close unlinked temporary files ([Issue #1327](https://github.com/benoitc/gunicorn/issues/1327), [PR #1428](https://github.com/benoitc/gunicorn/pull/1428)) +- fix: parse ``--umask=0`` correctly ([Issue #1622](https://github.com/benoitc/gunicorn/issues/1622), [PR #1632](https://github.com/benoitc/gunicorn/pull/1632)) +- fix: allow loading applications using relative file paths + ([Issue #1349](https://github.com/benoitc/gunicorn/issues/1349), [PR #1481](https://github.com/benoitc/gunicorn/pull/1481)) +- fix: force blocking mode on the gevent sockets ([Issue #880](https://github.com/benoitc/gunicorn/issues/880), [PR #1616](https://github.com/benoitc/gunicorn/pull/1616)) +- fix: preserve leading `/` in request path ([Issue #1512](https://github.com/benoitc/gunicorn/issues/1512), [PR #1511](https://github.com/benoitc/gunicorn/pull/1511)) +- fix: forbid contradictory secure scheme headers +- fix: handle malformed basic authentication headers in access log + ([Issue #1683](https://github.com/benoitc/gunicorn/issues/1683), [PR #1684](https://github.com/benoitc/gunicorn/pull/1684)) +- fix: defer handling of ``USR1`` signal to a new greenlet under gevent + ([Issue #1645](https://github.com/benoitc/gunicorn/issues/1645), [PR #1651](https://github.com/benoitc/gunicorn/pull/1651)) +- fix: the threaded worker would sometimes close the wrong keep-alive + connection under Python 2 ([Issue #1698](https://github.com/benoitc/gunicorn/issues/1698), [PR #1699](https://github.com/benoitc/gunicorn/pull/1699)) +- fix: re-open log files on ``USR1`` signal using ``handler._open`` to + support subclasses of ``FileHandler`` ([Issue #1739](https://github.com/benoitc/gunicorn/issues/1739), [PR #1742](https://github.com/benoitc/gunicorn/pull/1742)) +- deprecation: the ``gaiohttp`` worker is deprecated, see the + [worker-class](reference/settings.md#worker_class) documentation for more information + ([Issue #1338](https://github.com/benoitc/gunicorn/issues/1338), [PR #1418](https://github.com/benoitc/gunicorn/pull/1418), [PR #1569](https://github.com/benoitc/gunicorn/pull/1569)) diff --git a/docs/content/2019-news.md b/docs/content/2019-news.md new file mode 100644 index 00000000..8359edd6 --- /dev/null +++ b/docs/content/2019-news.md @@ -0,0 +1,112 @@ + +# Changelog - 2019 + +!!! note + Please see [news](news.md) for the latest changes + + +## 20.0.4 / 2019/11/26 + +- fix binding a socket using the file descriptor +- remove support for the `bdist_rpm` build + +## 20.0.3 / 2019/11/24 + +- fixed load of a config file without a Python extension +- fixed `socketfromfd.fromfd` when defaults are not set + +!!! note + ``` + ## 20.0.2 / 2019/11/23 + + - fix changelog + + ## 20.0.1 / 2019/11/23 + + - fixed the way the config module is loaded. `__file__` is now available + - fixed `wsgi.input_terminated`. It is always true. + - use the highest protocol version of openssl by default + - only support Python >= 3.5 + - added `__repr__` method to `Config` instance + - fixed support of AIX platform and musl libc in `socketfromfd.fromfd` function + - fixed support of applications loaded from a factory function + - fixed chunked encoding support to prevent any `request smuggling `_ + - Capture os.sendfile before patching in gevent and eventlet workers. + fix `RecursionError`. + - removed locking in reloader when adding new files + - load the WSGI application before the loader to pick up all files + +{note} +as documented in Flask and other places. +``` +## 19.10.0 / 2019/11/23 + +- unblock select loop during reload of a sync worker +- security fix: http desync attack +- handle `wsgi.input_terminated` +- added support for str and bytes in unix socket addresses +- fixed `max_requests` setting +- headers values are now encoded as LATN1, not ASCII +- fixed `InotifyReloadeder`: handle `module.__file__` is None +- fixed compatibility with tornado 6 +- fixed root logging +- Prevent removalof unix sockets from `reuse_port` +- Clear tornado ioloop before os.fork +- Miscellaneous fixes and improvement for linting using Pylint + +## 20.0 / 2019/10/30 + +- Fixed `fdopen` `RuntimeWarning` in Python 3.8 +- Added check and exception for str type on value in Response process_headers method. +- Ensure WSGI header value is string before conducting regex search on it. +- Added pypy3 to list of tested environments +- Grouped `StopIteration` and `KeyboardInterrupt` exceptions with same body together in Arbiter.run() +- Added `setproctitle` module to `extras_require` in setup.py +- Avoid unnecessary chown of temporary files +- Logging: Handle auth type case insensitively +- Removed `util.import_module` +- Removed fallback for `types.SimpleNamespace` in tests utils +- Use `SourceFileLoader` instead instead of `execfile_` +- Use `importlib` instead of `__import__` and eval` +- Fixed eventlet patching +- Added optional `datadog `_ tags for statsd metrics +- Header values now are encoded using latin-1, not ascii. +- Rewritten `parse_address` util added test +- Removed redundant super() arguments +- Simplify `futures` import in gthread module +- Fixed worker_connections` setting to also affects the Gthread worker type +- Fixed setting max_requests +- Bump minimum Eventlet and Gevent versions to 0.24 and 1.4 +- Use Python default SSL cipher list by default +- handle `wsgi.input_terminated` extension +- Simplify Paste Deployment documentation +- Fix root logging: root and logger are same level. +- Fixed typo in ssl_version documentation +- Documented systemd deployment unit examples +- Added systemd sd_notify support +- Fixed typo in gthread.py +- Added `tornado `_ 5 and 6 support +- Declare our setuptools dependency +- Added support to `--bind` to open file descriptors +- Document how to serve WSGI app modules from Gunicorn +- Provide guidance on X-Forwarded-For access log in documentation +- Add support for named constants in the `--ssl-version` flag +- Clarify log format usage of header & environment in documentation +- Fixed systemd documentation to properly setup gunicorn unix socket +- Prevent removal unix socket for reuse_port +- Fix `ResourceWarning` when reading a Python config module +- Remove unnecessary call to dict keys method +- Support str and bytes for UNIX socket addresses +- fixed `InotifyReloadeder`: handle `module.__file__` is None +- `/dev/shm` as a convenient alternative to making your own tmpfs mount in fchmod FAQ +- fix examples to work on python3 +- Fix typo in `--max-requests` documentation +- Clear tornado ioloop before os.fork +- Miscellaneous fixes and improvement for linting using Pylint + +### Breaking Change + +- Removed gaiohttp worker +- Drop support for Python 2.x +- Drop support for EOL Python 3.2 and 3.3 +- Drop support for Paste Deploy server blocks diff --git a/docs/content/2020-news.md b/docs/content/2020-news.md new file mode 100644 index 00000000..29195f68 --- /dev/null +++ b/docs/content/2020-news.md @@ -0,0 +1,7 @@ + +# Changelog - 2020 + +!!! note + Please see [news](news.md) for the latest changes + + diff --git a/docs/content/2021-news.md b/docs/content/2021-news.md new file mode 100644 index 00000000..d0572de1 --- /dev/null +++ b/docs/content/2021-news.md @@ -0,0 +1,51 @@ + +# Changelog - 2021 + +!!! note + Please see [news](news.md) for the latest changes + + +## 20.1.0 - 2021-02-12 + +- document WEB_CONCURRENCY is set by, at least, Heroku +- capture peername from accept: Avoid calls to getpeername by capturing the peer name returned by + accept +- log a warning when a worker was terminated due to a signal +- fix tornado usage with latest versions of Django +- add support for python -m gunicorn +- fix systemd socket activation example +- allows to set wsgi application in config file using `wsgi_app` +- document `--timeout = 0` +- always close a connection when the number of requests exceeds the max requests +- Disable keepalive during graceful shutdown +- kill tasks in the gthread workers during upgrade +- fix latency in gevent worker when accepting new requests +- fix file watcher: handle errors when new worker reboot and ensure the list of files is kept +- document the default name and path of the configuration file +- document how variable impact configuration +- document the `$PORT` environment variable +- added milliseconds option to request_time in access_log +- added PIP requirements to be used for example +- remove version from the Server header +- fix sendfile: use `socket.sendfile` instead of `os.sendfile` +- reloader: use absolute path to prevent empty to prevent0 `InotifyError` when a file + is added to the working directory +- Add --print-config option to print the resolved settings at startup. +- remove the `--log-dict-config` CLI flag because it never had a working format + (the `logconfig_dict` setting in configuration files continues to work) + + +### Breaking changes + +- minimum version is Python 3.5 +- remove version from the Server header + +** Documentation ** + + + +** Others ** + +- miscellaneous changes in the code base to be a better citizen with Python 3 +- remove dead code +- fix documentation generation diff --git a/docs/content/2023-news.md b/docs/content/2023-news.md new file mode 100644 index 00000000..9526c0c7 --- /dev/null +++ b/docs/content/2023-news.md @@ -0,0 +1,36 @@ + +# Changelog - 2023 + +## 21.2.0 - 2023-07-19 + +- fix thread worker: revert change considering connection as idle . + +!!! note + This is fixing the bad file description error. + + 21.1.0 - 2023-07-18 + + +=================== + +- fix thread worker: fix socket removal from the queue + +## 21.0.1 - 2023-07-17 + +- fix documentation build + +## 21.0.0 - 2023-07-17 + +- support python 3.11 +- fix gevent and eventlet workers +- fix threads support (gththread.md): improve performance and unblock requests +- SSL: now use SSLContext object +- HTTP parser: miscellaneous fixes +- remove unnecessary setuid calls +- fix testing +- improve logging +- miscellaneous fixes to core engine + +*** RELEASE NOTE *** + +We made this release major to start our new release cycle. More info will be provided on our discussion forum. diff --git a/docs/content/2024-news.md b/docs/content/2024-news.md new file mode 100644 index 00000000..8ae71611 --- /dev/null +++ b/docs/content/2024-news.md @@ -0,0 +1,58 @@ + +# Changelog - 2024 + +## 23.0.0 - 2024-08-10 + +- minor docs fixes ([PR #3217](https://github.com/benoitc/gunicorn/pull/3217), [PR #3089](https://github.com/benoitc/gunicorn/pull/3089), [PR #3167](https://github.com/benoitc/gunicorn/pull/3167)) +- worker_class parameter accepts a class ([PR #3079](https://github.com/benoitc/gunicorn/pull/3079)) +- fix deadlock if request terminated during chunked parsing ([PR #2688](https://github.com/benoitc/gunicorn/pull/2688)) +- permit receiving Transfer-Encodings: compress, deflate, gzip ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261)) +- permit Transfer-Encoding headers specifying multiple encodings. note: no parameters, still ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261)) +- sdist generation now explicitly excludes sphinx build folder ([PR #3257](https://github.com/benoitc/gunicorn/pull/3257)) +- decode bytes-typed status (as can be passed by gevent) as utf-8 instead of raising `TypeError` ([PR #2336](https://github.com/benoitc/gunicorn/pull/2336)) +- raise correct Exception when encounting invalid chunked requests ([PR #3258](https://github.com/benoitc/gunicorn/pull/3258)) +- the SCRIPT_NAME and PATH_INFO headers, when received from allowed forwarders, are no longer restricted for containing an underscore ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192)) +- include IPv6 loopback address ``[::1]`` in default for [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) and [proxy-allow-ips](reference/settings.md#proxy_allow_ips) ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192)) + +!!! note + - The SCRIPT_NAME change mitigates a regression that appeared first in the 22.0.0 release + - Review your [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) setting if you are still not seeing the SCRIPT_NAME transmitted + - Review your [forwarder-headers](reference/settings.md#forwarder_headers) setting if you are missing headers after upgrading from a version prior to 22.0.0 + + +### Breaking changes + +- refuse requests where the uri field is empty ([PR #3255](https://github.com/benoitc/gunicorn/pull/3255)) +- refuse requests with invalid CR/LR/NUL in heade field values ([PR #3253](https://github.com/benoitc/gunicorn/pull/3253)) +- remove temporary ``--tolerate-dangerous-framing`` switch from 22.0 ([PR #3260](https://github.com/benoitc/gunicorn/pull/3260)) +- If any of the breaking changes affect you, be aware that now refused requests can post a security problem, especially so in setups involving request pipe-lining and/or proxies. + +## 22.0.0 - 2024-04-17 + +- use `utime` to notify workers liveness +- migrate setup to pyproject.toml +- fix numerous security vulnerabilities in HTTP parser (closing some request smuggling vectors) +- parsing additional requests is no longer attempted past unsupported request framing +- on HTTP versions < 1.1 support for chunked transfer is refused (only used in exploits) +- requests conflicting configured or passed SCRIPT_NAME now produce a verbose error +- Trailer fields are no longer inspected for headers indicating secure scheme +- support Python 3.12 + +### Breaking changes + +- minimum version is Python 3.7 +- the limitations on valid characters in the HTTP method have been bounded to Internet Standards +- requests specifying unsupported transfer coding (order.md) are refused by default (rare.md) +- HTTP methods are no longer casefolded by default (IANA method registry contains none affected) +- HTTP methods containing the number sign (#) are no longer accepted by default (rare.md) +- HTTP versions < 1.0 or >= 2.0 are no longer accepted by default (rare, only HTTP/1.1 is supported) +- HTTP versions consisting of multiple digits or containing a prefix/suffix are no longer accepted +- HTTP header field names Gunicorn cannot safely map to variables are silently dropped, as in other software +- HTTP headers with empty field name are refused by default (no legitimate use cases, used in exploits) +- requests with both Transfer-Encoding and Content-Length are refused by default (such a message might indicate an attempt to perform request smuggling) +- empty transfer codings are no longer permitted (reportedly seen with really old & broken proxies) + + +### Security + +- fix CVE-2024-1135 diff --git a/docs/content/2026-news.md b/docs/content/2026-news.md new file mode 100644 index 00000000..55f6a181 --- /dev/null +++ b/docs/content/2026-news.md @@ -0,0 +1,37 @@ + +# Changelog - 2026 + +## 24.0.0 - 2026-01-23 + +### New Features + +- **ASGI Worker (Beta)**: Native asyncio-based ASGI support for running async Python + frameworks like FastAPI, Starlette, and Quart without external dependencies + ([PR #3444](https://github.com/benoitc/gunicorn/pull/3444)) + - HTTP/1.1 with keepalive connections + - WebSocket support + - Lifespan protocol for startup/shutdown hooks + - Optional uvloop for improved performance + - New settings: `--asgi-loop`, `--asgi-lifespan`, `--root-path` + +- **uWSGI Binary Protocol**: Support for receiving requests from nginx via + `uwsgi_pass` directive, enabling efficient binary protocol communication + ([PR #3444](https://github.com/benoitc/gunicorn/pull/3444)) + - New settings: `--protocol uwsgi`, `--uwsgi-allow-from` + +- **Documentation Migration**: Migrated documentation from Sphinx to MkDocs + with Material theme for improved navigation and mobile experience + ([PR #3426](https://github.com/benoitc/gunicorn/pull/3426)) + +### Changes + +- Minimum Python version is now 3.12 +- Documentation now hosted at https://gunicorn.org + +### Breaking changes + +- Dropped support for Python versions before 3.12 + +!!! warning "ASGI Worker Beta" + The ASGI worker is a beta feature. While tested, the API and behavior + may change in future releases. Please report any issues on GitHub. diff --git a/docs/content/404.md b/docs/content/404.md new file mode 100644 index 00000000..211fcad0 --- /dev/null +++ b/docs/content/404.md @@ -0,0 +1,22 @@ +# Page Not Found + +The page you're looking for doesn't exist or has moved. + + diff --git a/docs/content/asgi.md b/docs/content/asgi.md new file mode 100644 index 00000000..8cc51b4b --- /dev/null +++ b/docs/content/asgi.md @@ -0,0 +1,241 @@ +# ASGI Worker + +!!! warning "Beta Feature" + The ASGI worker is a beta feature introduced in Gunicorn 24.0.0. While it has been tested, + the API and behavior may change in future releases. Please report any issues on + [GitHub](https://github.com/benoitc/gunicorn/issues). + +Gunicorn includes a native ASGI worker that enables running async Python web frameworks +like FastAPI, Starlette, and Quart without external dependencies like Uvicorn. + +## Quick Start + +```bash +# Install gunicorn +pip install gunicorn + +# Run an ASGI application +gunicorn myapp:app --worker-class asgi --workers 4 +``` + +For FastAPI applications: + +```bash +gunicorn main:app --worker-class asgi --bind 0.0.0.0:8000 +``` + +## Features + +The ASGI worker provides: + +- **HTTP/1.1** with keepalive connections +- **WebSocket** support for real-time applications +- **Lifespan protocol** for startup/shutdown hooks +- **Optional uvloop** for improved performance +- **SSL/TLS** support + +## Configuration + +### Worker Class + +Set the worker class to `asgi`: + +```bash +gunicorn myapp:app --worker-class asgi +``` + +Or in a configuration file: + +```python +# gunicorn.conf.py +worker_class = "asgi" +``` + +### Event Loop + +Control which asyncio event loop implementation to use: + +| Value | Description | +|----------|-------------| +| `auto` | Use uvloop if available, otherwise asyncio (default) | +| `asyncio`| Use Python's built-in asyncio event loop | +| `uvloop` | Use uvloop (must be installed separately) | + +```bash +gunicorn myapp:app --worker-class asgi --asgi-loop uvloop +``` + +To use uvloop, install it first: + +```bash +pip install uvloop +``` + +### Lifespan Protocol + +The lifespan protocol lets your application run code at startup and shutdown. +This is essential for frameworks that need to initialize database connections, +caches, or background tasks. + +| Value | Description | +|--------|-------------| +| `auto` | Detect if app supports lifespan, enable if so (default) | +| `on` | Always run lifespan protocol (fail if unsupported) | +| `off` | Never run lifespan protocol | + +```bash +gunicorn myapp:app --worker-class asgi --asgi-lifespan on +``` + +Example FastAPI application using lifespan: + +```python +from contextlib import asynccontextmanager +from fastapi import FastAPI + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup: initialize resources + print("Starting up...") + yield + # Shutdown: cleanup resources + print("Shutting down...") + +app = FastAPI(lifespan=lifespan) +``` + +### Root Path + +When running behind a reverse proxy that mounts your application at a subpath, +set `root_path` so your application knows its mount point: + +```bash +gunicorn myapp:app --worker-class asgi --root-path /api +``` + +This is equivalent to the `SCRIPT_NAME` in WSGI applications. + +### Worker Connections + +Control the maximum number of concurrent connections per worker: + +```bash +gunicorn myapp:app --worker-class asgi --worker-connections 1000 +``` + +!!! note + Unlike sync workers, the `--threads` option has no effect on ASGI workers. + Use `--worker-connections` to control concurrency. + +## WebSocket Support + +The ASGI worker supports WebSocket connections out of the box. No additional +configuration is required. + +Example with Starlette: + +```python +from starlette.applications import Starlette +from starlette.routing import WebSocketRoute + +async def websocket_endpoint(websocket): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text(f"Echo: {data}") + +app = Starlette(routes=[ + WebSocketRoute("/ws", websocket_endpoint), +]) +``` + +## Production Deployment + +### With Nginx + +```nginx +upstream gunicorn { + server 127.0.0.1:8000; +} + +server { + listen 80; + server_name example.com; + + location / { + proxy_pass http://gunicorn; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # WebSocket support + location /ws { + proxy_pass http://gunicorn; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + } +} +``` + +### Recommended Settings + +For production ASGI deployments: + +```python +# gunicorn.conf.py +worker_class = "asgi" +workers = 4 # Number of worker processes +worker_connections = 1000 # Max connections per worker +keepalive = 5 # Keepalive timeout +timeout = 120 # Worker timeout +graceful_timeout = 30 # Graceful shutdown timeout + +# Performance tuning +asgi_loop = "auto" # Use uvloop if available +asgi_lifespan = "auto" # Auto-detect lifespan support +``` + +## Comparison with Other ASGI Servers + +| Feature | Gunicorn ASGI | Uvicorn | Hypercorn | +|---------|---------------|---------|-----------| +| Process management | Built-in | External | Built-in | +| HTTP/2 | No | No | Yes | +| WebSocket | Yes | Yes | Yes | +| Lifespan | Yes | Yes | Yes | +| uvloop support | Yes | Yes | Yes | + +Gunicorn's ASGI worker provides the same process management, logging, and +configuration capabilities you're familiar with from WSGI deployments. + +## Troubleshooting + +### Lifespan startup failed + +If you see "ASGI lifespan startup failed", your application may not properly +implement the lifespan protocol. Either fix the application or set +`--asgi-lifespan off`. + +### Connection limits + +If you're hitting connection limits, increase `--worker-connections` or add +more workers with `--workers`. + +### Slow responses under load + +Try using uvloop for better performance: + +```bash +pip install uvloop +gunicorn myapp:app --worker-class asgi --asgi-loop uvloop +``` + +## See Also + +- [Settings Reference](reference/settings.md#asgi_loop) - All ASGI-related settings +- [Deploy](deploy.md) - General deployment guidance +- [Design](design.md) - Worker architecture overview diff --git a/docs/content/assets/gunicorn.svg b/docs/content/assets/gunicorn.svg new file mode 100644 index 00000000..073f2029 --- /dev/null +++ b/docs/content/assets/gunicorn.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + g + + + + + diff --git a/docs/content/assets/javascripts/toc-collapse.js b/docs/content/assets/javascripts/toc-collapse.js new file mode 100644 index 00000000..6b2dc6f9 --- /dev/null +++ b/docs/content/assets/javascripts/toc-collapse.js @@ -0,0 +1,79 @@ +// Collapsible TOC for settings page +(function() { + function initCollapsibleTOC() { + // Only apply to pages with many TOC items (like settings) + var tocNav = document.querySelector('.md-nav--secondary'); + if (!tocNav) return; + + // Skip if already initialized + if (tocNav.dataset.tocCollapse === 'true') return; + tocNav.dataset.tocCollapse = 'true'; + + var tocItems = tocNav.querySelectorAll('.md-nav__item'); + if (tocItems.length < 20) return; + + // Find all top-level TOC items that have nested lists + var topList = tocNav.querySelector('.md-nav__list'); + if (!topList) return; + + var sections = topList.children; + + for (var i = 0; i < sections.length; i++) { + (function(section) { + var nestedNav = section.querySelector('.md-nav'); + if (!nestedNav) return; + + var link = section.querySelector('.md-nav__link'); + if (!link) return; + + // Skip if already has toggle + if (link.querySelector('.toc-toggle')) return; + + // Collapse by default + nestedNav.style.display = 'none'; + + // Create toggle button + var toggle = document.createElement('span'); + toggle.className = 'toc-toggle'; + toggle.innerHTML = '+'; + toggle.style.float = 'right'; + toggle.style.marginRight = '0.5rem'; + toggle.style.fontWeight = 'bold'; + toggle.style.cursor = 'pointer'; + toggle.style.userSelect = 'none'; + link.appendChild(toggle); + + // Toggle function for this specific section + function toggleSection(e) { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + + if (nestedNav.style.display === 'none') { + nestedNav.style.display = 'block'; + toggle.innerHTML = '−'; + } else { + nestedNav.style.display = 'none'; + toggle.innerHTML = '+'; + } + } + + // Click on toggle button + toggle.onclick = toggleSection; + })(sections[i]); + } + } + + // Run on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCollapsibleTOC); + } else { + initCollapsibleTOC(); + } + + // Re-run on instant navigation (MkDocs Material) + if (typeof document$ !== 'undefined') { + document$.subscribe(initCollapsibleTOC); + } +})(); diff --git a/docs/content/assets/stylesheets/home.css b/docs/content/assets/stylesheets/home.css new file mode 100644 index 00000000..5f1748ef --- /dev/null +++ b/docs/content/assets/stylesheets/home.css @@ -0,0 +1,439 @@ +/* ============================================ + Gunicorn Landing Page + Inspired by Caddy: minimal, spacious, clean + ============================================ */ + +.home { + --accent: #00a650; + --accent-hover: #00c853; + --accent-dark: #008542; + --teal: #00bfa5; + --text: #1a1a2e; + --text-muted: #555; + --bg: #fff; + --bg-alt: #f8faf8; + --border: #e0e6e0; + --code-bg: #0d1117; + --max-width: 900px; + + width: 100%; + max-width: none; + margin: 0; + padding: 0; + font-size: 1.0625rem; + line-height: 1.7; + color: var(--text); +} + +[data-md-color-scheme="slate"] .home { + --text: #e6e6e6; + --text-muted: #aaa; + --bg: #0d1117; + --bg-alt: #161b22; + --border: #30363d; +} + +/* Remove MkDocs constraints */ +.md-main__inner { margin: 0; max-width: none; } +.md-content { max-width: none; } +.md-content__inner { margin: 0; padding: 0; } + +/* ============================================ + Sections - Caddy-style vertical flow + ============================================ */ +.home section { + padding: 5rem 2rem; +} + +.home section:nth-child(even) { + background: var(--bg-alt); +} + +.home .container { + max-width: var(--max-width); + margin: 0 auto; +} + +/* ============================================ + Hero + ============================================ */ +.hero { + text-align: center; + padding: 6rem 2rem 5rem; +} + +.hero .container { + max-width: 700px; +} + +.hero__logo { + width: 350px !important; + max-width: 350px !important; + min-width: 350px; + height: auto; + margin-bottom: 2rem; +} + +.hero h1 { + font-size: 3rem; + font-weight: 700; + line-height: 1.15; + margin: 0 0 1.5rem 0; + letter-spacing: -0.02em; + white-space: nowrap; +} + +.hero__tagline { + font-size: 1.25rem; + color: var(--text-muted); + margin: 0 0 2.5rem 0; + max-width: 550px; + margin-left: auto; + margin-right: auto; +} + +.hero__buttons { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; + margin-bottom: 3rem; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.875rem 1.75rem; + font-size: 1rem; + font-weight: 500; + text-decoration: none; + border-radius: 6px; + transition: all 0.15s ease; +} + +.btn--primary { + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%); + color: #fff; + box-shadow: 0 4px 12px rgba(0, 166, 80, 0.3); +} + +.btn--primary:hover { + box-shadow: 0 6px 20px rgba(0, 166, 80, 0.4); + transform: translateY(-2px); +} + +.btn--secondary { + background: transparent; + color: var(--text); + border: 1px solid var(--border); +} + +.btn--secondary:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* Terminal */ +.terminal { + background: var(--code-bg); + border-radius: 8px; + overflow: hidden; + text-align: left; + max-width: 500px; + margin: 0 auto; + box-shadow: 0 8px 30px rgba(0,0,0,0.12); +} + +.terminal__header { + background: #161b22; + padding: 0.75rem 1rem; + display: flex; + gap: 6px; +} + +.terminal__dot { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.terminal__dot--red { background: #ff5f56; } +.terminal__dot--yellow { background: #ffbd2e; } +.terminal__dot--green { background: #27c93f; } + +.terminal__body { + padding: 1.25rem 1.5rem; + font-family: 'SF Mono', Monaco, Consolas, monospace; + font-size: 0.9rem; + line-height: 1.8; + color: #c9d1d9; +} + +.terminal__line { + display: block; +} + +.terminal__prompt { + color: var(--accent-hover); + user-select: none; +} + +.terminal__comment { + color: #6e7681; +} + +/* ============================================ + Why Gunicorn - 3 pillars + ============================================ */ +.why h2 { + text-align: center; + font-size: 2rem; + margin: 0 0 3rem 0; +} + +.pillars { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; +} + +.pillar h3 { + font-size: 1.125rem; + margin: 0 0 0.5rem 0; +} + +.pillar p { + color: var(--text-muted); + margin: 0; + font-size: 0.9375rem; +} + +/* ============================================ + Frameworks + ============================================ */ +.frameworks h2 { + text-align: center; + font-size: 1.75rem; + margin: 0 0 0.5rem 0; +} + +.frameworks__subtitle { + text-align: center; + color: var(--text-muted); + margin: 0 0 2rem 0; +} + +.frameworks__list { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.75rem; +} + +.framework-tag { + padding: 0.5rem 1rem; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 100px; + font-size: 0.875rem; + font-weight: 500; + transition: all 0.15s ease; +} + +[data-md-color-scheme="slate"] .framework-tag { + background: var(--bg-alt); +} + +.framework-tag:hover { + border-color: var(--accent); + color: var(--accent); +} + +.framework-tag--new { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +/* ============================================ + Workers + ============================================ */ +.workers h2 { + font-size: 1.75rem; + margin: 0 0 2rem 0; +} + +.workers__grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.worker { + padding: 1.5rem; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + text-decoration: none; + color: inherit; + transition: border-color 0.15s ease; +} + +[data-md-color-scheme="slate"] .worker { + background: var(--bg-alt); +} + +.worker:hover { + border-color: var(--accent); +} + +.worker h3 { + font-size: 1rem; + margin: 0 0 0.25rem 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.worker p { + color: var(--text-muted); + font-size: 0.875rem; + margin: 0; +} + +.badge { + font-size: 0.625rem; + font-weight: 700; + padding: 0.125rem 0.375rem; + background: var(--accent); + color: #fff; + border-radius: 3px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* ============================================ + Quick Links + ============================================ */ +.quick-links { + text-align: center; +} + +.quick-links h2 { + font-size: 1.75rem; + margin: 0 0 2rem 0; +} + +.quick-links__grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; + text-align: left; +} + +.quick-link { + padding: 1.25rem; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + text-decoration: none; + color: inherit; + transition: border-color 0.15s ease; +} + +[data-md-color-scheme="slate"] .quick-link { + background: var(--bg-alt); +} + +.quick-link:hover { + border-color: var(--accent); +} + +.quick-link strong { + display: block; + margin-bottom: 0.25rem; +} + +.quick-link span { + font-size: 0.875rem; + color: var(--text-muted); +} + +/* ============================================ + Footer CTA + ============================================ */ +.home-footer { + text-align: center; +} + +.home-footer h2 { + font-size: 1.75rem; + margin: 0 0 1rem 0; +} + +.home-footer p { + color: var(--text-muted); + margin: 0 0 2rem 0; +} + +.home-footer__links { + display: flex; + justify-content: center; + gap: 2rem; +} + +.home-footer__links a { + color: var(--text-muted); + text-decoration: none; + font-size: 0.9375rem; +} + +.home-footer__links a:hover { + color: var(--accent); +} + +/* ============================================ + Responsive + ============================================ */ +@media (max-width: 768px) { + .home section { + padding: 3.5rem 1.5rem; + } + + .hero h1 { + font-size: 2.25rem; + } + + .pillars { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .workers__grid { + grid-template-columns: 1fr; + } + + .quick-links__grid { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 480px) { + .hero h1 { + font-size: 1.875rem; + } + + .hero__buttons { + flex-direction: column; + } + + .btn { + width: 100%; + justify-content: center; + } + + .quick-links__grid { + grid-template-columns: 1fr; + } +} diff --git a/docs/content/community.md b/docs/content/community.md new file mode 100644 index 00000000..e9996b9d --- /dev/null +++ b/docs/content/community.md @@ -0,0 +1,40 @@ +# Community + +Connect with the project through these channels. + +## Project management & discussions + +Project maintenance guidelines live on the +[wiki](https://github.com/benoitc/gunicorn/wiki/Project-management). + +GitHub is used for: + +- [Bug reports](https://github.com/benoitc/gunicorn/issues) — search before + opening a new issue. +- [Discussions](https://github.com/benoitc/gunicorn/discussions) — Q&A and usage + tips. +- [Feature planning](https://github.com/benoitc/gunicorn/issues) — development + and project management topics. + +## IRC + +Join the Gunicorn channel on [Libera Chat](https://libera.chat/) at +[`#gunicorn`](https://web.libera.chat/?channels=#gunicorn). + +## Issue tracking + +File bugs, enhancements, and tasks in the +[GitHub issue tracker](https://github.com/benoitc/gunicorn/issues). + +## Security issues + +Report security vulnerabilities privately to +[`security@gunicorn.org`](mailto:security@gunicorn.org); only core developers +subscribe to this list. + +## Contributing + +Start with the +[contributing guide](https://github.com/benoitc/gunicorn/blob/master/CONTRIBUTING.md) +for development workflow, code style, and review expectations. New contributors +are welcome—open a draft pull request early to gather feedback. diff --git a/docs/content/configure.md b/docs/content/configure.md new file mode 100644 index 00000000..1698fba5 --- /dev/null +++ b/docs/content/configure.md @@ -0,0 +1,78 @@ + +# Configuration Overview + +Gunicorn reads configuration from five places, in increasing order of priority: + +1. Environment variables, for settings that support them. +2. Framework-specific configuration (currently Paste Deploy only). +3. A Python configuration file `gunicorn.conf.py` (default in the working directory). +4. The `GUNICORN_CMD_ARGS` environment variable. +5. Command-line arguments. + +If a configuration file is provided both via `GUNICORN_CMD_ARGS` and the CLI, +only the file specified on the command line is used. + +!!! note + Print the fully resolved configuration: + +bash +gunicorn --print-config APP_MODULE +``` + +Validate configuration and exit: + +```bash +gunicorn --check-config APP_MODULE +``` + +This is also a quick way to confirm that your application can start. +``` + +## Command line + +Options set on the command line override framework settings and values from the +configuration file. Not every setting has a command-line flag; run + +```bash +gunicorn -h +``` + +for the complete list. The CLI also exposes `--version`, which is not part of +the main [settings reference](reference/settings.md). + + +## Configuration file + +Provide a Python file (for example `gunicorn.conf.py`). Gunicorn executes the +file on every start or reload, so any valid Python is allowed: + +```python +import multiprocessing + +bind = "127.0.0.1:8000" +workers = multiprocessing.cpu_count() * 2 + 1 +``` + +Every configuration key is documented in the [settings reference](reference/settings.md). + +## Framework settings + +At present only Paste Deploy applications expose framework-specific settings. +If you have ideas for Django or other frameworks, open an +[issue](https://github.com/benoitc/gunicorn/issues). + +### Paste applications + +Reference Gunicorn as the server in your INI file: + +```ini +[server:main] +use = egg:gunicorn#main +host = 192.168.0.1 +port = 80 +workers = 2 +proc_name = brim +``` + +Gunicorn merges any recognised parameters into the base configuration. Values +from the configuration file and command line still override these defaults. diff --git a/docs/content/custom.md b/docs/content/custom.md new file mode 100644 index 00000000..f2bdfb73 --- /dev/null +++ b/docs/content/custom.md @@ -0,0 +1,62 @@ + +# Custom Application + +!!! info "Added in 19.0" + Use Gunicorn as part of your own WSGI application by subclassing + `gunicorn.app.base.BaseApplication`. + + + +Example: create a tiny WSGI app and load it with a custom application: + +```text +--8<-- "examples/standalone_app.py" +``` + + + +## Using server hooks + +Provide hooks through configuration, just like a standard Gunicorn deployment. +For example, a `pre_fork` hook: + +```python +def pre_fork(server, worker): + print(f"pre-fork server {server} worker {worker}", file=sys.stderr) + +if __name__ == "__main__": + options = { + "bind": "127.0.0.1:8080", + "workers": number_of_workers(), + "pre_fork": pre_fork, + } +``` + +## Direct usage of existing WSGI apps + +Run Gunicorn from Python to serve a WSGI application instance at runtime—useful +for rolling deploys or packaging with PEX. Gunicorn exposes +`gunicorn.app.wsgiapp`, which accepts any WSGI app (for example a Flask or +Django instance). Assuming your package is `exampleapi` and the application is +`app`: + +```bash +python -m gunicorn.app.wsgiapp exampleapi:app +``` + +All CLI flags and configuration files still apply: + +```bash +# Custom parameters +python -m gunicorn.app.wsgiapp exampleapi:app --bind=0.0.0.0:8081 --workers=4 +# Using a config file +python -m gunicorn.app.wsgiapp exampleapi:app -c config.py +``` + +For PEX builds use `-c gunicorn` at build time so the packaged app accepts the +entry point at runtime: + +```bash +pex . -v -c gunicorn -o compiledapp.pex +./compiledapp.pex exampleapi:app -c gunicorn_config.py +``` diff --git a/docs/content/deploy.md b/docs/content/deploy.md new file mode 100644 index 00000000..bb78674e --- /dev/null +++ b/docs/content/deploy.md @@ -0,0 +1,322 @@ +# Deploying Gunicorn + +We strongly recommend running Gunicorn behind a proxy server. + +## Nginx configuration + +Although many HTTP proxies exist, we recommend [Nginx](https://nginx.org/). +When using the default synchronous workers you must ensure the proxy buffers +slow clients; otherwise Gunicorn becomes vulnerable to denial-of-service +attacks. Use [Hey](https://github.com/rakyll/hey) to verify proxy behaviour. + +An example configuration for fast clients with Nginx +([source](https://github.com/benoitc/gunicorn/blob/master/examples/nginx.conf)): + +```nginx title="nginx.conf" +--8<-- "examples/nginx.conf" +``` + + + +To support streaming requests/responses or patterns such as Comet, long +polling, or WebSockets, disable proxy buffering and run Gunicorn with an async +worker class: + +```nginx +location @proxy_to_app { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_buffering off; + + proxy_pass http://app_server; +} +``` + +To ignore aborted requests (for example, health checks that close connections +prematurely) enable +[`proxy_ignore_client_abort`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_client_abort): + +```nginx +proxy_ignore_client_abort on; +``` + +!!! note + The default value for `proxy_ignore_client_abort` is `off`. If it remains off + Nginx logs will report error 499 and Gunicorn may log `Ignoring EPIPE` when the + log level is `debug`. + + + +Pass protocol information to Gunicorn so applications can generate correct +URLs. Add this header to your `location` block: + +```nginx +proxy_set_header X-Forwarded-Proto $scheme; +``` + +If Nginx runs on a different host, tell Gunicorn which proxies are trusted so it +accepts the `X-Forwarded-*` headers: + +```bash +gunicorn -w 3 --forwarded-allow-ips="10.170.3.217,10.170.3.220" test:app +``` + +When all traffic comes from trusted proxies (for example Heroku) you can set +`--forwarded-allow-ips='*'`. This is **dangerous** if untrusted clients can +reach Gunicorn directly, because forged headers could make your application +serve secure content over plain HTTP. + +Gunicorn 19 changed the handling of `REMOTE_ADDR` to conform to +[RFC 3875](https://www.rfc-editor.org/rfc/rfc3875), meaning it now records the +proxy IP rather than the upstream client. To log the real client address, set +[`access_log_format`](reference/settings.md#access_log_format) to include `X-Forwarded-For`: + +```text +%({x-forwarded-for}i)s %(l.md)s %(u.md)s %(t.md)s "%(r.md)s" %(s.md)s %(b.md)s "%(f.md)s" "%(a.md)s" +``` + +When binding Gunicorn to a UNIX socket `REMOTE_ADDR` will be empty. + +## Using virtual environments + +Install Gunicorn inside your project +[virtual environment](https://pypi.python.org/pypi/virtualenv) to keep versions +isolated: + +```bash +mkdir ~/venvs/ +virtualenv ~/venvs/webapp +source ~/venvs/webapp/bin/activate +pip install gunicorn +deactivate +``` + +Force installation into the active virtual environment with `--ignore-installed`: + +```bash +source ~/venvs/webapp/bin/activate +pip install -I gunicorn +``` + +## Monitoring + +!!! note + Do not enable Gunicorn's daemon mode when using process monitors. These + supervisors expect to manage the direct child process. + + + +### Gaffer + +Use [Gaffer](https://gaffer.readthedocs.io/) with *gafferd* to manage Gunicorn: + +```ini +[process:gunicorn] +cmd = gunicorn -w 3 test:app +cwd = /path/to/project +``` + +Create a `Procfile` if you prefer: + +```procfile +gunicorn = gunicorn -w 3 test:app +``` + +Start Gunicorn via Gaffer: + +```bash +gaffer start +``` + +Or load it into a running *gafferd* instance: + +```bash +gaffer load +``` + +### runit + +[runit](http://smarden.org/runit/) is a popular supervisor. A sample service +script (see the +[full example](https://github.com/benoitc/gunicorn/blob/master/examples/gunicorn_rc)): + +```bash +#!/bin/sh + +GUNICORN=/usr/local/bin/gunicorn +ROOT=/path/to/project +PID=/var/run/gunicorn.pid + +APP=main:application + +if [ -f $PID ]; then rm $PID; fi + +cd $ROOT +exec $GUNICORN -c $ROOT/gunicorn.conf.py --pid=$PID $APP +``` + +Save as `/etc/sv//run`, make it executable, and symlink into +`/etc/service/`. runit will then supervise Gunicorn. + +### Supervisor + +[Supervisor](http://supervisord.org/) configuration example (adapted from +[examples/supervisor.conf](https://github.com/benoitc/gunicorn/blob/master/examples/supervisor.conf)): + +```ini +[program:gunicorn] +command=/path/to/gunicorn main:application -c /path/to/gunicorn.conf.py +directory=/path/to/project +user=nobody +autostart=true +autorestart=true +redirect_stderr=true +``` + +### Upstart + +Sample Upstart config (logs go to `/var/log/upstart/myapp.log`): + +```upstart +# /etc/init/myapp.conf + +description "myapp" + +start on (filesystem.md) +stop on runlevel [016] + +respawn +setuid nobody +setgid nogroup +chdir /path/to/app/directory + +exec /path/to/virtualenv/bin/gunicorn myapp:app +``` + +### systemd + +[systemd](https://www.freedesktop.org/wiki/Software/systemd/) can create a UNIX +socket and launch Gunicorn on demand. + +Service file: + +```ini +# /etc/systemd/system/gunicorn.service + +[Unit] +Description=gunicorn daemon +Requires=gunicorn.socket +After=network.target + +[Service] +Type=notify +NotifyAccess=main +User=someuser +Group=someuser +RuntimeDirectory=gunicorn +WorkingDirectory=/home/someuser/applicationroot +ExecStart=/usr/bin/gunicorn applicationname.wsgi +ExecReload=/bin/kill -s HUP $MAINPID +KillMode=mixed +TimeoutStopSec=5 +PrivateTmp=true + +[Install] +WantedBy=multi-user.target +``` + +`Type=notify` lets Gunicorn report readiness to systemd. If the service should +run under a transient user consider adding `DynamicUser=true`. Tighten +permissions further with `ProtectSystem=strict` if the app permits. + +Socket activation file: + +```ini +# /etc/systemd/system/gunicorn.socket + +[Unit] +Description=gunicorn socket + +[Socket] +ListenStream=/run/gunicorn.sock +SocketUser=www-data +SocketGroup=www-data +SocketMode=0660 + +[Install] +WantedBy=sockets.target +``` + +Enable and start the socket so it begins listening immediately and on reboot: + +```bash +systemctl enable --now gunicorn.socket +``` + +Test connectivity from the nginx user (Debian defaults to `www-data`): + +```bash +sudo -u www-data curl --unix-socket /run/gunicorn.sock http +``` + +!!! note + Use `systemctl show --value -p MainPID gunicorn.service` to retrieve the main + process ID or `systemctl kill -s HUP gunicorn.service` to send signals. + + + +Configure Nginx to proxy to the new socket: + +```nginx +user www-data; +... +http { + server { + listen 8000; + server_name 127.0.0.1; + location / { + proxy_pass http://unix:/run/gunicorn.sock; + } + } +} +... +``` + +!!! note + Adjust `listen` and `server_name` for production (typically port 80 and your + site's domain). + + + +Ensure nginx starts automatically: + +```bash +systemctl enable nginx.service +systemctl start nginx +``` + +Browse to to verify Gunicorn + Nginx + systemd. + +## Logging + +Configure logging through the CLI flags described in the +[settings documentation](reference/settings.md#logging) or via a +[logging configuration file](https://github.com/benoitc/gunicorn/blob/master/examples/logging.conf). +Rotate logs with `logrotate` by sending `SIGUSR1`: + +```bash +kill -USR1 $(cat /var/run/gunicorn.pid) +``` + +!!! note + If you override the `LOGGING` dictionary, set `disable_existing_loggers` to + `False` so Gunicorn's loggers remain active. + + + +!!! warning + Gunicorn's error log should capture Gunicorn-related messages only. Route your + application logs separately. + + diff --git a/docs/content/design.md b/docs/content/design.md new file mode 100644 index 00000000..ec2cce27 --- /dev/null +++ b/docs/content/design.md @@ -0,0 +1,216 @@ + +# Design + +A brief look at Gunicorn's architecture. + +## Server Model + +Gunicorn uses a **pre-fork worker model**: an arbiter process manages worker +processes, while the workers handle requests and responses. The arbiter never +touches individual client sockets. + +
+ +
+
⚖️
+ +### Arbiter + +Orchestrates the worker pool. Listens for signals (`TTIN`, `TTOU`, `CHLD`, +`HUP`) to adjust workers, restart them on failure, or reload configuration. +
+ +
+
⚙️
+ +### Worker Pool + +Each worker handles requests independently. Worker types determine +concurrency model: sync, threaded, or async via greenlets/asyncio. +
+ +
+
📡
+ +### Signal Communication + +`TTIN`/`TTOU` adjust worker count. `CHLD` triggers restart of crashed +workers. `HUP` reloads configuration. See [Signals](signals.md). +
+ +
+ +## Worker Types + +Choose a worker type based on your application's needs. + +=== "Sync" + + The **default** worker. Handles one request at a time per worker. + + - Simple and predictable + - Errors affect only the current request + - No keep-alive support (connections close after response) + - Requires a buffering proxy (nginx, HAProxy) for production + + ```bash + gunicorn myapp:app + ``` + +=== "Gthread" + + Threaded worker with a **thread pool** per worker process. + + - Supports keep-alive connections + - Good balance of concurrency and simplicity + - Threads share memory (lower footprint than workers) + - Idle connections close after keepalive timeout + + ```bash + gunicorn myapp:app -k gthread --threads 4 + ``` + +=== "ASGI" + + Native **asyncio** support for modern async frameworks. + + - For FastAPI, Starlette, Quart, and other ASGI apps + - Full async/await support + - See the [ASGI Guide](asgi.md) for details + + ```bash + gunicorn myapp:app -k uvicorn.workers.UvicornWorker + ``` + +=== "Gevent" + + **Greenlet-based** async worker using [Gevent](http://www.gevent.org/). + + - Handles thousands of concurrent connections + - Supports keep-alive, WebSockets, long-polling + - May require patches for some libraries (e.g., `psycogreen` for Psycopg) + - Not compatible with code that relies on blocking behavior + + ```bash + gunicorn myapp:app -k gevent --worker-connections 1000 + ``` + +=== "Eventlet" + + **Greenlet-based** async worker using [Eventlet](http://eventlet.net/). + + - Similar capabilities to Gevent + - Handles high concurrency for I/O-bound apps + - Some libraries may need compatibility patches + + ```bash + gunicorn myapp:app -k eventlet --worker-connections 1000 + ``` + +=== "Tornado" + + Worker for [Tornado](https://www.tornadoweb.org/) applications. + + - Designed for Tornado's async framework + - Can serve WSGI apps, but not recommended for that use case + - Use when running native Tornado applications + + ```bash + gunicorn myapp:app -k tornado + ``` + +## Comparison + +| Worker | Concurrency Model | Keep-Alive | Best For | +|--------|-------------------|------------|----------| +| `sync` | 1 request/worker | ❌ | CPU-bound apps behind a proxy | +| `gthread` | Thread pool | ✅ | Mixed workloads, moderate concurrency | +| ASGI workers | AsyncIO | ✅ | Modern async frameworks (FastAPI, etc.) | +| `gevent` | Greenlets | ✅ | I/O-bound, WebSockets, streaming | +| `eventlet` | Greenlets | ✅ | I/O-bound, long-polling | +| `tornado` | Tornado IOLoop | ✅ | Native Tornado applications | + +!!! tip "Quick Decision Guide" + + - **Simple app behind nginx?** → `sync` (default) + - **Need keep-alive or moderate concurrency?** → `gthread` + - **WebSockets, streaming, long-polling?** → `gevent` or `eventlet` + - **FastAPI, Starlette, or async framework?** → ASGI worker + +## When to Use Async Workers + +Synchronous workers assume your app is CPU or network bound and avoids +indefinite blocking operations. Use async workers when you have: + +- Long blocking calls (external APIs, slow databases) +- Direct internet traffic without a buffering proxy +- Streaming request/response bodies +- Long polling or Comet patterns +- WebSockets + +!!! info "Testing Slow Clients" + + Tools like [Hey](https://github.com/rakyll/hey) can simulate slow responses + to test how your configuration handles them. + +## Scaling + +### How Many Workers? + +!!! warning "Don't Over-Scale" + + Workers ≠ clients. Gunicorn typically needs only **4–12 workers** to handle + heavy traffic. Too many workers waste resources and can reduce throughput. + +Start with this formula and adjust under load: + +``` +workers = (2 × CPU cores) + 1 +``` + +Use `TTIN`/`TTOU` signals to adjust the worker count at runtime. + +### How Many Threads? + +With the `gthread` worker, you can combine workers and threads: + +```bash +gunicorn myapp:app -k gthread --workers 4 --threads 2 +``` + +!!! info "Threads vs Workers" + + - **Threads** share memory → lower footprint + - **Workers** isolate failures → better fault tolerance + - Combine both for the best of both worlds + +Threads can extend request time beyond the worker timeout while still +notifying the arbiter. The optimal mix depends on your runtime (CPython vs +PyPy) and workload. + +## Configuration Examples + +```bash +# Sync (default) - simple apps behind nginx +gunicorn myapp:app + +# Gthread - keep-alive and thread concurrency +gunicorn myapp:app -k gthread --workers 4 --threads 4 + +# Gevent - high concurrency for I/O-bound apps +gunicorn myapp:app -k gevent --workers 4 --worker-connections 1000 + +# Eventlet - alternative async worker +gunicorn myapp:app -k eventlet --workers 4 --worker-connections 1000 + +# ASGI - FastAPI/Starlette with Uvicorn worker +gunicorn myapp:app -k uvicorn.workers.UvicornWorker --workers 4 +``` + + + +!!! note "Third-Party AsyncIO Workers" + + For asyncio frameworks, you can also use third-party workers. See the + [aiohttp deployment guide](https://docs.aiohttp.org/en/stable/deployment.html#nginx-gunicorn) + for examples. diff --git a/docs/content/faq.md b/docs/content/faq.md new file mode 100644 index 00000000..f26f1797 --- /dev/null +++ b/docs/content/faq.md @@ -0,0 +1,160 @@ + +# FAQ + +## WSGI bits + +### How do I set `SCRIPT_NAME`? + +By default `SCRIPT_NAME` is an empty string. Set it via an environment variable +or HTTP header. Because the header contains an underscore it is only accepted +from trusted forwarders listed in [`forwarded_allow_ips`](reference/settings.md#forwarded_allow_ips). + +!!! note + If your application should appear under a subfolder, `SCRIPT_NAME` typically + starts with a single leading slash and no trailing slash. + + + +## Server stuff + +### How do I reload my application in Gunicorn? + +Send `HUP` to the master process for a graceful reload: + +```bash +kill -HUP masterpid +``` + +### How might I test a proxy configuration? + +Use [Hey](https://github.com/rakyll/hey) to confirm that your proxy buffers +responses correctly for synchronous workers: + +```bash +hey -n 10000 -c 100 http://127.0.0.1:5000/ +``` + +That benchmark issues 10,000 requests with a concurrency of 100. + +### How can I name processes? + +Install [setproctitle](https://pypi.python.org/pypi/setproctitle) to give +Gunicorn processes meaningful names in tools such as `ps` and `top`. This helps +when running multiple Gunicorn instances. See the +[`proc_name`](reference/settings.md#proc_name) setting for details. + +### Why is there no HTTP keep-alive? + +The default sync workers target Nginx, which uses HTTP/1.0 for upstream +connections. If you need to serve unbuffered internet traffic directly, pick an +async worker instead. + +## Worker processes + +### How do I know which type of worker to use? + +Read the [design guide](design.md) for guidance on worker types. + +### What types of workers are available? + +See the [`worker_class`](reference/settings.md#worker_class) configuration reference. + +### How can I figure out the best number of worker processes? + +Follow the recommendations for tuning the [`number of workers`](design.md#how-many-workers). + +### How can I change the number of workers dynamically? + +Send `TTIN` or `TTOU` to the master process: + +```bash +kill -TTIN $masterpid # increment workers +kill -TTOU $masterpid # decrement workers +``` + +### Does Gunicorn suffer from the thundering herd problem? + +Potentially, when many sleeping handlers wake simultaneously but only one takes +the request. There is ongoing work to mitigate this +([issue #792](https://github.com/benoitc/gunicorn/issues/792)). Monitor load if +you use large numbers of workers or threads. + +### Why don't I see logs in the console? + +Gunicorn 19.0 disabled console logging by default. Use `--log-file=-` to stream +logs to stdout. Console logging returned in 19.2. + +## Kernel parameters + +High-concurrency deployments may need kernel tuning. These Linux-oriented tips +apply to any network service. + +### How can I increase the maximum number of file descriptors? + +Raise the per-process limit (remember sockets count as files). Running `sudo +ulimit` is ineffective—switch to root, adjust the limit, then launch Gunicorn. +Consider managing limits via systemd service units or init scripts. + +### How can I increase the maximum socket backlog? + +Increase the queue of pending connections: + +```bash +sudo sysctl -w net.core.somaxconn="2048" +``` + +### How can I disable the use of `sendfile()`? + +Pass `--no-sendfile` or set the `SENDFILE=0` environment variable. + +## Troubleshooting + +### Django reports `ImproperlyConfigured` + +Asynchronous workers may break `django.core.urlresolvers.reverse`. Use +`reverse_lazy` instead. + +### How do I avoid blocking in `os.fchmod`? + +Gunicorn's heartbeat touches temporary files. On disk-backed filesystems (for +example `/tmp` on some distributions) `os.fchmod` can block if I/O stalls or the +filesystem fills up. Mount a `tmpfs` and point `--worker-tmp-dir` to it. + +Check whether `/tmp` is RAM-backed: + +```bash +df /tmp +``` + +If not, create a new `tmpfs` mount: + +```bash +sudo cp /etc/fstab /etc/fstab.orig +sudo mkdir /mem +echo 'tmpfs /mem tmpfs defaults,size=64m,mode=1777,noatime,comment=for-gunicorn 0 0' | sudo tee -a /etc/fstab +sudo mount /mem +``` + +Verify the result: + +```bash +df /mem +``` + +Then start Gunicorn with `--worker-tmp-dir /mem`. + +### Why are workers silently killed? + +If a worker vanishes without logs, check for `SIGKILL`. Reverse proxies may show +`502` responses while Gunicorn logs only new worker startups (for example, +`[INFO] Booting worker`). A common culprit is the OOM killer in cgroups-limited +environments. + +Inspect kernel logs: + +```bash +dmesg | grep gunicorn +``` + +If you see messages similar to `Memory cgroup out of memory ... Killed process +(gunicorn.md)`, raise memory limits or adjust OOM behaviour. diff --git a/docs/content/guides/docker.md b/docs/content/guides/docker.md new file mode 100644 index 00000000..036b38b1 --- /dev/null +++ b/docs/content/guides/docker.md @@ -0,0 +1,339 @@ +# Docker Deployment + +Running Gunicorn in Docker containers is the most common deployment pattern +for modern Python applications. This guide covers best practices for +containerizing Gunicorn applications. + +## Basic Dockerfile + +```dockerfile +FROM python:3.12-slim + +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application +COPY . . + +# Run gunicorn +CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"] +``` + +Build and run: + +```bash +docker build -t myapp . +docker run -p 8000:8000 myapp +``` + +## Production Configuration + +### Environment Variables + +Use environment variables for configuration: + +```dockerfile +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Configuration via environment +ENV GUNICORN_WORKERS=4 +ENV GUNICORN_BIND=0.0.0.0:8000 + +CMD gunicorn app:app \ + --workers ${GUNICORN_WORKERS} \ + --bind ${GUNICORN_BIND} +``` + +Or use `GUNICORN_CMD_ARGS`: + +```dockerfile +ENV GUNICORN_CMD_ARGS="--workers=4 --bind=0.0.0.0:8000" +CMD ["gunicorn", "app:app"] +``` + +### Worker Count + +In containers, determine workers based on available CPU: + +```python +# gunicorn.conf.py +import multiprocessing + +workers = multiprocessing.cpu_count() * 2 + 1 +bind = "0.0.0.0:8000" +``` + +Or let Kubernetes/Docker limit CPU and calculate accordingly: + +```bash +# At runtime +gunicorn app:app --workers $(( 2 * $(nproc) + 1 )) +``` + +### Non-Root User + +Run as a non-root user for security: + +```dockerfile +FROM python:3.12-slim + +# Create non-root user +RUN useradd --create-home appuser +WORKDIR /home/appuser/app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY --chown=appuser:appuser . . + +USER appuser + +CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"] +``` + +### Health Checks + +Add a health check endpoint and Docker health check: + +```dockerfile +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 +``` + +## Multi-Stage Build + +Reduce image size with multi-stage builds: + +```dockerfile +# Build stage +FROM python:3.12 AS builder + +WORKDIR /app +COPY requirements.txt . +RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt + +# Runtime stage +FROM python:3.12-slim + +WORKDIR /app + +# Copy wheels and install +COPY --from=builder /wheels /wheels +RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels + +COPY . . + +CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4"] +``` + +## Docker Compose + +Example `docker-compose.yml`: + +```yaml +services: + web: + build: . + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgres://db:5432/myapp + depends_on: + - db + deploy: + resources: + limits: + cpus: '2' + memory: 512M + + db: + image: postgres:15 + environment: + - POSTGRES_DB=myapp + - POSTGRES_PASSWORD=secret + volumes: + - postgres_data:/var/lib/postgresql/data + + nginx: + image: nginx:alpine + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - web + +volumes: + postgres_data: +``` + +## Kubernetes Deployment + +Example Kubernetes deployment: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp +spec: + replicas: 3 + selector: + matchLabels: + app: myapp + template: + metadata: + labels: + app: myapp + spec: + containers: + - name: myapp + image: myapp:latest + ports: + - containerPort: 8000 + env: + - name: GUNICORN_WORKERS + value: "4" + resources: + limits: + cpu: "1" + memory: "512Mi" + requests: + cpu: "500m" + memory: "256Mi" + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: myapp +spec: + selector: + app: myapp + ports: + - port: 80 + targetPort: 8000 +``` + +## Graceful Shutdown + +Gunicorn handles `SIGTERM` gracefully by default. Configure the timeout: + +```dockerfile +CMD ["gunicorn", "app:app", \ + "--bind", "0.0.0.0:8000", \ + "--graceful-timeout", "30", \ + "--timeout", "120"] +``` + +Match Docker's stop timeout: + +```yaml +# docker-compose.yml +services: + web: + stop_grace_period: 30s +``` + +## Logging + +Log to stdout/stderr for Docker log collection: + +```python +# gunicorn.conf.py +accesslog = "-" +errorlog = "-" +loglevel = "info" +``` + +Use JSON logging for log aggregation: + +```python +# gunicorn.conf.py +import json +import datetime + +class JsonFormatter: + def format(self, record): + return json.dumps({ + "timestamp": datetime.datetime.utcnow().isoformat(), + "level": record.levelname, + "message": record.getMessage(), + }) + +logconfig_dict = { + "version": 1, + "formatters": { + "json": {"()": JsonFormatter} + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "json", + "stream": "ext://sys.stdout" + } + }, + "root": { + "handlers": ["console"], + "level": "INFO" + } +} +``` + +## Troubleshooting + +### Worker Timeout + +If workers are killed with `[CRITICAL] WORKER TIMEOUT`, increase the timeout: + +```bash +gunicorn app:app --timeout 120 +``` + +Or investigate slow requests in your application. + +### Out of Memory + +If containers are OOM-killed: + +1. Reduce worker count +2. Use `--max-requests` to restart workers periodically +3. Increase container memory limits + +```bash +gunicorn app:app --workers 2 --max-requests 1000 --max-requests-jitter 100 +``` + +### Connection Reset + +If you see connection resets, ensure: + +1. Load balancer health checks match your `/health` endpoint +2. Graceful timeout is sufficient for in-flight requests +3. Keepalive settings match between Gunicorn and upstream proxy + +## See Also + +- [Deploy](../deploy.md) - General deployment patterns +- [Settings](../reference/settings.md) - All configuration options diff --git a/docs/content/index.md b/docs/content/index.md new file mode 100644 index 00000000..7a9136b5 --- /dev/null +++ b/docs/content/index.md @@ -0,0 +1,127 @@ +--- +template: home.html +title: Gunicorn - Python WSGI HTTP Server +--- + +
+
+ +

The Python WSGI Server

+

+ Battle-tested. Production-ready. One command to serve your Python apps. +

+ +
+
+ + + +
+
+ $ pip install gunicorn + $ gunicorn myapp:app + # Listening at http://127.0.0.1:8000 +
+
+
+
+ +
+
+

Why Gunicorn?

+
+
+

Production-Proven

+

Trusted by thousands of companies. The pre-fork worker model handles traffic spikes gracefully.

+
+
+

Lightweight

+

Minimal dependencies, simple configuration. Efficient from containers to bare metal.

+
+
+

Compatible

+

Works with any WSGI or ASGI framework. Django, Flask, FastAPI—it just runs.

+
+
+
+
+ +
+
+

Works With Your Stack

+

WSGI and ASGI frameworks, no changes needed

+
+ Django + Flask + FastAPI + Pyramid + Starlette + Falcon + Bottle + Quart +
+
+
+ +
+ +
+ + + + diff --git a/docs/content/install.md b/docs/content/install.md new file mode 100644 index 00000000..27c73fed --- /dev/null +++ b/docs/content/install.md @@ -0,0 +1,176 @@ +# Installation + +!!! note + Gunicorn requires **Python 3.12 or newer**. + +## Quick Install + +=== "pip" + + ```bash + pip install gunicorn + ``` + +=== "pipx" + + ```bash + pipx install gunicorn + ``` + +=== "Docker" + + ```bash + docker run -p 8000:8000 -v $(pwd):/app -w /app \ + python:3.12-slim sh -c "pip install gunicorn && gunicorn app:app" + ``` + + See the [Docker guide](guides/docker.md) for production configurations. + +=== "System Packages" + + **Debian/Ubuntu:** + ```bash + sudo apt-get update + sudo apt-get install gunicorn + ``` + + **Fedora:** + ```bash + sudo dnf install python3-gunicorn + ``` + + **Arch Linux:** + ```bash + sudo pacman -S gunicorn + ``` + + !!! warning + System packages may lag behind the latest release. For production, + prefer pip installation in a virtual environment. + +## Virtual Environment (Recommended) + +Always install Gunicorn inside a virtual environment to isolate dependencies: + +```bash +# Create virtual environment +python -m venv venv + +# Activate it +source venv/bin/activate # Linux/macOS +# or: venv\Scripts\activate # Windows + +# Install gunicorn +pip install gunicorn +``` + +## From Source + +Install the latest development version from GitHub: + +```bash +pip install git+https://github.com/benoitc/gunicorn.git +``` + +Upgrade to the latest commit: + +```bash +pip install -U git+https://github.com/benoitc/gunicorn.git +``` + +## Extra Packages + +Gunicorn provides optional extras for additional worker types and features. +Install them with pip's bracket syntax: + +```bash +pip install gunicorn[gevent,setproctitle] +``` + +### Worker Types + +| Extra | Description | +|-------|-------------| +| `gunicorn[eventlet]` | Eventlet-based greenlet workers | +| `gunicorn[gevent]` | Gevent-based greenlet workers | +| `gunicorn[gthread]` | Threaded workers | +| `gunicorn[tornado]` | Tornado-based workers (not recommended) | + +See the [design docs](design.md) for guidance on choosing worker types. + +### Utilities + +| Extra | Description | +|-------|-------------| +| `gunicorn[setproctitle]` | Set process name in `ps`/`top` output | + +!!! tip + If running multiple Gunicorn instances, use `setproctitle` with the + [`proc_name`](reference/settings.md#proc_name) setting to distinguish them. + +## Async Workers + +For applications using async I/O patterns, install the appropriate greenlet +library: + +=== "Gevent" + + ```bash + pip install gunicorn[gevent] + ``` + + Run with: + ```bash + gunicorn app:app --worker-class gevent + ``` + +=== "Eventlet" + + ```bash + pip install gunicorn[eventlet] + ``` + + Run with: + ```bash + gunicorn app:app --worker-class eventlet + ``` + +=== "ASGI (asyncio)" + + No extra installation required: + + ```bash + gunicorn app:app --worker-class asgi + ``` + + For better performance, install uvloop: + ```bash + pip install uvloop + gunicorn app:app --worker-class asgi --asgi-loop uvloop + ``` + +!!! note + Greenlet-based workers require the Python development headers. On Ubuntu: + `sudo apt-get install python3-dev` + +## Verify Installation + +Check the installed version: + +```bash +gunicorn --version +``` + +Test with a simple application: + +```bash +echo 'def app(e, s): s("200 OK", []); return [b"OK"]' > test_app.py +gunicorn test_app:app +# Visit http://127.0.0.1:8000 +``` + +## Next Steps + +- [Quickstart](quickstart.md) - Get running in 5 minutes +- [Run](run.md) - CLI usage and framework integration +- [Configure](configure.md) - Configuration options diff --git a/docs/content/instrumentation.md b/docs/content/instrumentation.md new file mode 100644 index 00000000..c4d3c248 --- /dev/null +++ b/docs/content/instrumentation.md @@ -0,0 +1,32 @@ + +# Instrumentation + +!!! info "Added in 19.1" + Gunicorn exposes optional instrumentation for the arbiter and workers using the + statsD protocol over UDP. The `gunicorn.instrument.statsd` module turns + Gunicorn into a statsD client. + + + +UDP keeps Gunicorn isolated from slow statsD consumers, so metrics collection +does not impact request handling. + +Tell Gunicorn where the statsD server is located: + +```bash +gunicorn --statsd-host=localhost:8125 --statsd-prefix=service.app ... +``` + +The `Statsd` logger subclasses `gunicorn.glogging.Logger` and tracks: + +- `gunicorn.requests` — request rate per second +- `gunicorn.request.duration` — request duration histogram (milliseconds.md) +- `gunicorn.workers` — number of workers managed by the arbiter (gauge.md) +- `gunicorn.log.critical` — rate of critical log messages +- `gunicorn.log.error` — rate of error log messages +- `gunicorn.log.warning` — rate of warning log messages +- `gunicorn.log.exception` — rate of exceptional log messages + +See the [`statsd_host`](reference/settings.md#statsd_host) setting for additional options. + +[statsD](https://github.com/etsy/statsd) diff --git a/docs/content/news.md b/docs/content/news.md new file mode 100644 index 00000000..7a855fa2 --- /dev/null +++ b/docs/content/news.md @@ -0,0 +1,98 @@ + +# Changelog + +## 24.0.0 - 2026-01-23 + +### New Features + +- **ASGI Worker (Beta)**: Native asyncio-based ASGI support for running async Python + frameworks like FastAPI, Starlette, and Quart without external dependencies + - HTTP/1.1 with keepalive connections + - WebSocket support + - Lifespan protocol for startup/shutdown hooks + - Optional uvloop for improved performance + +- **uWSGI Binary Protocol**: Support for receiving requests from nginx via + `uwsgi_pass` directive + +- **Documentation Migration**: Migrated to MkDocs with Material theme + +### Breaking changes + +- Minimum Python version is now 3.12 + +--- + +## 23.0.0 - 2024-08-10 + +- minor docs fixes ([PR #3217](https://github.com/benoitc/gunicorn/pull/3217), [PR #3089](https://github.com/benoitc/gunicorn/pull/3089), [PR #3167](https://github.com/benoitc/gunicorn/pull/3167)) +- worker_class parameter accepts a class ([PR #3079](https://github.com/benoitc/gunicorn/pull/3079)) +- fix deadlock if request terminated during chunked parsing ([PR #2688](https://github.com/benoitc/gunicorn/pull/2688)) +- permit receiving Transfer-Encodings: compress, deflate, gzip ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261)) +- permit Transfer-Encoding headers specifying multiple encodings. note: no parameters, still ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261)) +- sdist generation now explicitly excludes sphinx build folder ([PR #3257](https://github.com/benoitc/gunicorn/pull/3257)) +- decode bytes-typed status (as can be passed by gevent) as utf-8 instead of raising `TypeError` ([PR #2336](https://github.com/benoitc/gunicorn/pull/2336)) +- raise correct Exception when encounting invalid chunked requests ([PR #3258](https://github.com/benoitc/gunicorn/pull/3258)) +- the SCRIPT_NAME and PATH_INFO headers, when received from allowed forwarders, are no longer restricted for containing an underscore ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192)) +- include IPv6 loopback address ``[::1]`` in default for [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) and [proxy-allow-ips](reference/settings.md#proxy_allow_ips) ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192)) + +!!! note + - The SCRIPT_NAME change mitigates a regression that appeared first in the 22.0.0 release + - Review your [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) setting if you are still not seeing the SCRIPT_NAME transmitted + - Review your [forwarder-headers](reference/settings.md#forwarder_headers) setting if you are missing headers after upgrading from a version prior to 22.0.0 + + +### Breaking changes + +- refuse requests where the uri field is empty ([PR #3255](https://github.com/benoitc/gunicorn/pull/3255)) +- refuse requests with invalid CR/LR/NUL in heade field values ([PR #3253](https://github.com/benoitc/gunicorn/pull/3253)) +- remove temporary ``--tolerate-dangerous-framing`` switch from 22.0 ([PR #3260](https://github.com/benoitc/gunicorn/pull/3260)) +- If any of the breaking changes affect you, be aware that now refused requests can post a security problem, especially so in setups involving request pipe-lining and/or proxies. + +## 22.0.0 - 2024-04-17 + +- use `utime` to notify workers liveness +- migrate setup to pyproject.toml +- fix numerous security vulnerabilities in HTTP parser (closing some request smuggling vectors) +- parsing additional requests is no longer attempted past unsupported request framing +- on HTTP versions < 1.1 support for chunked transfer is refused (only used in exploits) +- requests conflicting configured or passed SCRIPT_NAME now produce a verbose error +- Trailer fields are no longer inspected for headers indicating secure scheme +- support Python 3.12 + +### Breaking changes + +- minimum version is Python 3.7 +- the limitations on valid characters in the HTTP method have been bounded to Internet Standards +- requests specifying unsupported transfer coding (order.md) are refused by default (rare.md) +- HTTP methods are no longer casefolded by default (IANA method registry contains none affected) +- HTTP methods containing the number sign (#) are no longer accepted by default (rare.md) +- HTTP versions < 1.0 or >= 2.0 are no longer accepted by default (rare, only HTTP/1.1 is supported) +- HTTP versions consisting of multiple digits or containing a prefix/suffix are no longer accepted +- HTTP header field names Gunicorn cannot safely map to variables are silently dropped, as in other software +- HTTP headers with empty field name are refused by default (no legitimate use cases, used in exploits) +- requests with both Transfer-Encoding and Content-Length are refused by default (such a message might indicate an attempt to perform request smuggling) +- empty transfer codings are no longer permitted (reportedly seen with really old & broken proxies) + + +### Security + +- fix CVE-2024-1135 + +## History + +- [2026](2026-news.md) +- [2024](2024-news.md) +- [2023](2023-news.md) +- [2021](2021-news.md) +- [2020](2020-news.md) +- [2019](2019-news.md) +- [2018](2018-news.md) +- [2017](2017-news.md) +- [2016](2016-news.md) +- [2015](2015-news.md) +- [2014](2014-news.md) +- [2013](2013-news.md) +- [2012](2012-news.md) +- [2011](2011-news.md) +- [2010](2010-news.md) diff --git a/docs/content/quickstart.md b/docs/content/quickstart.md new file mode 100644 index 00000000..5457483a --- /dev/null +++ b/docs/content/quickstart.md @@ -0,0 +1,115 @@ +# Quickstart + +Get a Python web application running with Gunicorn in 5 minutes. + +## Install + +```bash +pip install gunicorn +``` + +## Create an Application + +Create `app.py`: + +=== "Flask" + + ```python + from flask import Flask + + app = Flask(__name__) + + @app.route("/") + def hello(): + return "Hello, World!" + ``` + +=== "FastAPI" + + ```python + from fastapi import FastAPI + + app = FastAPI() + + @app.get("/") + def hello(): + return {"message": "Hello, World!"} + ``` + +=== "Django" + + Django projects already have a WSGI application at `myproject/wsgi.py`. + No additional code is needed. + +=== "Plain WSGI" + + ```python + def app(environ, start_response): + data = b"Hello, World!" + start_response("200 OK", [ + ("Content-Type", "text/plain"), + ("Content-Length", str(len(data))) + ]) + return [data] + ``` + +## Run + +```bash +gunicorn app:app +``` + +For Django: + +```bash +gunicorn myproject.wsgi +``` + +For FastAPI (ASGI): + +```bash +gunicorn app:app --worker-class asgi +``` + +## Add Workers + +Use multiple workers to handle concurrent requests: + +```bash +gunicorn app:app --workers 4 +``` + +A good starting point is `2 * CPU_CORES + 1` workers. + +## Bind to a Port + +By default Gunicorn binds to `127.0.0.1:8000`. Change it with: + +```bash +gunicorn app:app --bind 0.0.0.0:8080 +``` + +## Configuration File + +Create `gunicorn.conf.py` for reusable settings: + +```python +bind = "0.0.0.0:8000" +workers = 4 +accesslog = "-" +``` + +Then run: + +```bash +gunicorn app:app +``` + +Gunicorn automatically loads `gunicorn.conf.py` from the current directory. + +## Next Steps + +- [Run](run.md) - Full CLI reference and framework integration +- [Configure](configure.md) - Configuration file options +- [Deploy](deploy.md) - Production deployment with nginx and process managers +- [Settings](reference/settings.md) - Complete settings reference diff --git a/docs/content/reference/settings.md b/docs/content/reference/settings.md new file mode 100644 index 00000000..e24227bf --- /dev/null +++ b/docs/content/reference/settings.md @@ -0,0 +1,1605 @@ +> **Generated file** — update `gunicorn/config.py` instead. + +# Settings + +This reference is built directly from `gunicorn.config.KNOWN_SETTINGS` and is +regenerated during every documentation build. + +!!! note + Settings can be provided through the `GUNICORN_CMD_ARGS` environment + variable. For example: + + ```console + $ GUNICORN_CMD_ARGS="--bind=127.0.0.1 --workers=3" gunicorn app:app + ``` + + _Added in 19.7._ + + + + +## Config File + +### `config` + +**Command line:** `-c CONFIG`, `--config CONFIG` + +**Default:** `'./gunicorn.conf.py'` + +[The Gunicorn config file](../configure.md#configuration-file). + +A string of the form ``PATH``, ``file:PATH``, or ``python:MODULE_NAME``. + +Only has an effect when specified on the command line or as part of an +application specific configuration. + +By default, a file named ``gunicorn.conf.py`` will be read from the same +directory where gunicorn is being run. + +!!! info "Changed in 19.4" + Loading the config from a Python module requires the ``python:`` + prefix. + +### `wsgi_app` + +**Default:** `None` + +A WSGI application path in pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``. + +!!! info "Added in 20.1.0" + +## Debugging + +### `reload` + +**Command line:** `--reload` + +**Default:** `False` + +Restart workers when code changes. + +This setting is intended for development. It will cause workers to be +restarted whenever application code changes. + +The reloader is incompatible with application preloading. When using a +paste configuration be sure that the server block does not import any +application code or the reload will not work as designed. + +The default behavior is to attempt inotify with a fallback to file +system polling. Generally, inotify should be preferred if available +because it consumes less system resources. + +!!! note + In order to use the inotify reloader, you must have the ``inotify`` + package installed. + +### `reload_engine` + +**Command line:** `--reload-engine STRING` + +**Default:** `'auto'` + +The implementation that should be used to power [reload](#reload). + +Valid engines are: + +* ``'auto'`` +* ``'poll'`` +* ``'inotify'`` (requires inotify) + +!!! info "Added in 19.7" + +### `reload_extra_files` + +**Command line:** `--reload-extra-file FILES` + +**Default:** `[]` + +Extends [reload](#reload) option to also watch and reload on additional files +(e.g., templates, configurations, specifications, etc.). + +!!! info "Added in 19.8" + +### `spew` + +**Command line:** `--spew` + +**Default:** `False` + +Install a trace function that spews every line executed by the server. + +This is the nuclear option. + +### `check_config` + +**Command line:** `--check-config` + +**Default:** `False` + +Check the configuration and exit. The exit status is 0 if the +configuration is correct, and 1 if the configuration is incorrect. + +### `print_config` + +**Command line:** `--print-config` + +**Default:** `False` + +Print the configuration settings as fully resolved. Implies [check-config](#check_config). + +## Logging + +### `accesslog` + +**Command line:** `--access-logfile FILE` + +**Default:** `None` + +The Access log file to write to. + +``'-'`` means log to stdout. + +### `disable_redirect_access_to_syslog` + +**Command line:** `--disable-redirect-access-to-syslog` + +**Default:** `False` + +Disable redirect access logs to syslog. + +!!! info "Added in 19.8" + +### `access_log_format` + +**Command line:** `--access-logformat STRING` + +**Default:** `'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'` + +The access log format. + +=========== =========== +Identifier Description +=========== =========== +h remote address +l ``'-'`` +u user name (if HTTP Basic auth used) +t date of the request +r status line (e.g. ``GET / HTTP/1.1``) +m request method +U URL path without query string +q query string +H protocol +s status +B response length +b response length or ``'-'`` (CLF format) +f referrer (note: header is ``referer``) +a user agent +T request time in seconds +M request time in milliseconds +D request time in microseconds +L request time in decimal seconds +p process ID +{header}i request header +{header}o response header +{variable}e environment variable +=========== =========== + +Use lowercase for header and environment variable names, and put +``{...}x`` names inside ``%(...)s``. For example:: + + %({x-forwarded-for}i)s + +### `errorlog` + +**Command line:** `--error-logfile FILE`, `--log-file FILE` + +**Default:** `'-'` + +The Error log file to write to. + +Using ``'-'`` for FILE makes gunicorn log to stderr. + +!!! info "Changed in 19.2" + Log to stderr by default. + +### `loglevel` + +**Command line:** `--log-level LEVEL` + +**Default:** `'info'` + +The granularity of Error log outputs. + +Valid level names are: + +* ``'debug'`` +* ``'info'`` +* ``'warning'`` +* ``'error'`` +* ``'critical'`` + +### `capture_output` + +**Command line:** `--capture-output` + +**Default:** `False` + +Redirect stdout/stderr to specified file in [errorlog](#errorlog). + +!!! info "Added in 19.6" + +### `logger_class` + +**Command line:** `--logger-class STRING` + +**Default:** `'gunicorn.glogging.Logger'` + +The logger you want to use to log events in Gunicorn. + +The default class (``gunicorn.glogging.Logger``) handles most +normal usages in logging. It provides error and access logging. + +You can provide your own logger by giving Gunicorn a Python path to a +class that quacks like ``gunicorn.glogging.Logger``. + +### `logconfig` + +**Command line:** `--log-config FILE` + +**Default:** `None` + +The log config file to use. +Gunicorn uses the standard Python logging module's Configuration +file format. + +### `logconfig_dict` + +**Default:** `{}` + +The log config dictionary to use, using the standard Python +logging module's dictionary configuration format. This option +takes precedence over the [logconfig](#logconfig) and [logconfig-json](#logconfig_json) options, +which uses the older file configuration format and JSON +respectively. + +Format: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig + +For more context you can look at the default configuration dictionary for logging, +which can be found at ``gunicorn.glogging.CONFIG_DEFAULTS``. + +!!! info "Added in 19.8" + +### `logconfig_json` + +**Command line:** `--log-config-json FILE` + +**Default:** `None` + +The log config to read config from a JSON file + +Format: https://docs.python.org/3/library/logging.config.html#logging.config.jsonConfig + +!!! info "Added in 20.0" + +### `syslog_addr` + +**Command line:** `--log-syslog-to SYSLOG_ADDR` + +**Default:** `'unix:///var/run/syslog'` + +Address to send syslog messages. + +Address is a string of the form: + +* ``unix://PATH#TYPE`` : for unix domain socket. ``TYPE`` can be ``stream`` + for the stream driver or ``dgram`` for the dgram driver. + ``stream`` is the default. +* ``udp://HOST:PORT`` : for UDP sockets +* ``tcp://HOST:PORT`` : for TCP sockets + +### `syslog` + +**Command line:** `--log-syslog` + +**Default:** `False` + +Send *Gunicorn* logs to syslog. + +!!! info "Changed in 19.8" + You can now disable sending access logs by using the + disable-redirect-access-to-syslog setting. + +### `syslog_prefix` + +**Command line:** `--log-syslog-prefix SYSLOG_PREFIX` + +**Default:** `None` + +Makes Gunicorn use the parameter as program-name in the syslog entries. + +All entries will be prefixed by ``gunicorn.``. By default the +program name is the name of the process. + +### `syslog_facility` + +**Command line:** `--log-syslog-facility SYSLOG_FACILITY` + +**Default:** `'user'` + +Syslog facility name + +### `enable_stdio_inheritance` + +**Command line:** `-R`, `--enable-stdio-inheritance` + +**Default:** `False` + +Enable stdio inheritance. + +Enable inheritance for stdio file descriptors in daemon mode. + +Note: To disable the Python stdout buffering, you can to set the user +environment variable ``PYTHONUNBUFFERED`` . + +### `statsd_host` + +**Command line:** `--statsd-host STATSD_ADDR` + +**Default:** `None` + +The address of the StatsD server to log to. + +Address is a string of the form: + +* ``unix://PATH`` : for a unix domain socket. +* ``HOST:PORT`` : for a network address + +!!! info "Added in 19.1" + +### `dogstatsd_tags` + +**Command line:** `--dogstatsd-tags DOGSTATSD_TAGS` + +**Default:** `''` + +A comma-delimited list of datadog statsd (dogstatsd) tags to append to +statsd metrics. + +!!! info "Added in 20" + +### `statsd_prefix` + +**Command line:** `--statsd-prefix STATSD_PREFIX` + +**Default:** `''` + +Prefix to use when emitting statsd metrics (a trailing ``.`` is added, +if not provided). + +!!! info "Added in 19.2" + +## Process Naming + +### `proc_name` + +**Command line:** `-n STRING`, `--name STRING` + +**Default:** `None` + +A base to use with setproctitle for process naming. + +This affects things like ``ps`` and ``top``. If you're going to be +running more than one instance of Gunicorn you'll probably want to set a +name to tell them apart. This requires that you install the setproctitle +module. + +If not set, the *default_proc_name* setting will be used. + +### `default_proc_name` + +**Default:** `'gunicorn'` + +Internal setting that is adjusted for each type of application. + +## SSL + +### `keyfile` + +**Command line:** `--keyfile FILE` + +**Default:** `None` + +SSL key file + +### `certfile` + +**Command line:** `--certfile FILE` + +**Default:** `None` + +SSL certificate file + +### `ssl_version` + +**Command line:** `--ssl-version` + +**Default:** `<_SSLMethod.PROTOCOL_TLS: 2>` + +SSL version to use (see stdlib ssl module's). + +!!! danger "Deprecated in 21.0" + The option is deprecated and it is currently ignored. Use [ssl-context](#ssl_context) instead. + +============= ============ +--ssl-version Description +============= ============ +SSLv3 SSLv3 is not-secure and is strongly discouraged. +SSLv23 Alias for TLS. Deprecated in Python 3.6, use TLS. +TLS Negotiate highest possible version between client/server. + Can yield SSL. (Python 3.6+) +TLSv1 TLS 1.0 +TLSv1_1 TLS 1.1 (Python 3.4+) +TLSv1_2 TLS 1.2 (Python 3.4+) +TLS_SERVER Auto-negotiate the highest protocol version like TLS, + but only support server-side SSLSocket connections. + (Python 3.6+) +============= ============ + +!!! info "Changed in 19.7" + The default value has been changed from ``ssl.PROTOCOL_TLSv1`` to + ``ssl.PROTOCOL_SSLv23``. + +!!! info "Changed in 20.0" + This setting now accepts string names based on ``ssl.PROTOCOL_`` + constants. + +!!! info "Changed in 20.0.1" + The default value has been changed from ``ssl.PROTOCOL_SSLv23`` to + ``ssl.PROTOCOL_TLS`` when Python >= 3.6 . + +### `cert_reqs` + +**Command line:** `--cert-reqs` + +**Default:** `` + +Whether client certificate is required (see stdlib ssl module's) + +=========== =========================== +--cert-reqs Description +=========== =========================== +`0` no client verification +`1` ssl.CERT_OPTIONAL +`2` ssl.CERT_REQUIRED +=========== =========================== + +### `ca_certs` + +**Command line:** `--ca-certs FILE` + +**Default:** `None` + +CA certificates file + +### `suppress_ragged_eofs` + +**Command line:** `--suppress-ragged-eofs` + +**Default:** `True` + +Suppress ragged EOFs (see stdlib ssl module's) + +### `do_handshake_on_connect` + +**Command line:** `--do-handshake-on-connect` + +**Default:** `False` + +Whether to perform SSL handshake on socket connect (see stdlib ssl module's) + +### `ciphers` + +**Command line:** `--ciphers` + +**Default:** `None` + +SSL Cipher suite to use, in the format of an OpenSSL cipher list. + +By default we use the default cipher list from Python's ``ssl`` module, +which contains ciphers considered strong at the time of each Python +release. + +As a recommended alternative, the Open Web App Security Project (OWASP) +offers `a vetted set of strong cipher strings rated A+ to C- +`_. +OWASP provides details on user-agent compatibility at each security level. + +See the `OpenSSL Cipher List Format Documentation +`_ +for details on the format of an OpenSSL cipher list. + +## Security + +### `limit_request_line` + +**Command line:** `--limit-request-line INT` + +**Default:** `4094` + +The maximum size of HTTP request line in bytes. + +This parameter is used to limit the allowed size of a client's +HTTP request-line. Since the request-line consists of the HTTP +method, URI, and protocol version, this directive places a +restriction on the length of a request-URI allowed for a request +on the server. A server needs this value to be large enough to +hold any of its resource names, including any information that +might be passed in the query part of a GET request. Value is a number +from 0 (unlimited) to 8190. + +This parameter can be used to prevent any DDOS attack. + +### `limit_request_fields` + +**Command line:** `--limit-request-fields INT` + +**Default:** `100` + +Limit the number of HTTP headers fields in a request. + +This parameter is used to limit the number of headers in a request to +prevent DDOS attack. Used with the *limit_request_field_size* it allows +more safety. By default this value is 100 and can't be larger than +32768. + +### `limit_request_field_size` + +**Command line:** `--limit-request-field_size INT` + +**Default:** `8190` + +Limit the allowed size of an HTTP request header field. + +Value is a positive number or 0. Setting it to 0 will allow unlimited +header field sizes. + +!!! warning + Setting this parameter to a very high or unlimited value can open + up for DDOS attacks. + +## Server Hooks + +### `on_starting` + +**Default:** + +```python +def on_starting(server): + pass +``` + +Called just before the master process is initialized. + +The callable needs to accept a single instance variable for the Arbiter. + +### `on_reload` + +**Default:** + +```python +def on_reload(server): + pass +``` + +Called to recycle workers during a reload via SIGHUP. + +The callable needs to accept a single instance variable for the Arbiter. + +### `when_ready` + +**Default:** + +```python +def when_ready(server): + pass +``` + +Called just after the server is started. + +The callable needs to accept a single instance variable for the Arbiter. + +### `pre_fork` + +**Default:** + +```python +def pre_fork(server, worker): + pass +``` + +Called just before a worker is forked. + +The callable needs to accept two instance variables for the Arbiter and +new Worker. + +### `post_fork` + +**Default:** + +```python +def post_fork(server, worker): + pass +``` + +Called just after a worker has been forked. + +The callable needs to accept two instance variables for the Arbiter and +new Worker. + +### `post_worker_init` + +**Default:** + +```python +def post_worker_init(worker): + pass +``` + +Called just after a worker has initialized the application. + +The callable needs to accept one instance variable for the initialized +Worker. + +### `worker_int` + +**Default:** + +```python +def worker_int(worker): + pass +``` + +Called just after a worker exited on SIGINT or SIGQUIT. + +The callable needs to accept one instance variable for the initialized +Worker. + +### `worker_abort` + +**Default:** + +```python +def worker_abort(worker): + pass +``` + +Called when a worker received the SIGABRT signal. + +This call generally happens on timeout. + +The callable needs to accept one instance variable for the initialized +Worker. + +### `pre_exec` + +**Default:** + +```python +def pre_exec(server): + pass +``` + +Called just before a new master process is forked. + +The callable needs to accept a single instance variable for the Arbiter. + +### `pre_request` + +**Default:** + +```python +def pre_request(worker, req): + worker.log.debug("%s %s", req.method, req.path) +``` + +Called just before a worker processes the request. + +The callable needs to accept two instance variables for the Worker and +the Request. + +### `post_request` + +**Default:** + +```python +def post_request(worker, req, environ, resp): + pass +``` + +Called after a worker processes the request. + +The callable needs to accept two instance variables for the Worker and +the Request. + +### `child_exit` + +**Default:** + +```python +def child_exit(server, worker): + pass +``` + +Called just after a worker has been exited, in the master process. + +The callable needs to accept two instance variables for the Arbiter and +the just-exited Worker. + +!!! info "Added in 19.7" + +### `worker_exit` + +**Default:** + +```python +def worker_exit(server, worker): + pass +``` + +Called just after a worker has been exited, in the worker process. + +The callable needs to accept two instance variables for the Arbiter and +the just-exited Worker. + +### `nworkers_changed` + +**Default:** + +```python +def nworkers_changed(server, new_value, old_value): + pass +``` + +Called just after *num_workers* has been changed. + +The callable needs to accept an instance variable of the Arbiter and +two integers of number of workers after and before change. + +If the number of workers is set for the first time, *old_value* would +be ``None``. + +### `on_exit` + +**Default:** + +```python +def on_exit(server): + pass +``` + +Called just before exiting Gunicorn. + +The callable needs to accept a single instance variable for the Arbiter. + +### `ssl_context` + +**Default:** + +```python +def ssl_context(config, default_ssl_context_factory): + return default_ssl_context_factory() +``` + +Called when SSLContext is needed. + +Allows customizing SSL context. + +The callable needs to accept an instance variable for the Config and +a factory function that returns default SSLContext which is initialized +with certificates, private key, cert_reqs, and ciphers according to +config and can be further customized by the callable. +The callable needs to return SSLContext object. + +Following example shows a configuration file that sets the minimum TLS version to 1.3: + +```python +def ssl_context(conf, default_ssl_context_factory): + import ssl + context = default_ssl_context_factory() + context.minimum_version = ssl.TLSVersion.TLSv1_3 + return context +``` + +!!! info "Added in 21.0" + +## Server Mechanics + +### `preload_app` + +**Command line:** `--preload` + +**Default:** `False` + +Load application code before the worker processes are forked. + +By preloading an application you can save some RAM resources as well as +speed up server boot times. Although, if you defer application loading +to each worker process, you can reload your application code easily by +restarting workers. + +### `sendfile` + +**Command line:** `--no-sendfile` + +**Default:** `None` + +Disables the use of ``sendfile()``. + +If not set, the value of the ``SENDFILE`` environment variable is used +to enable or disable its usage. + +!!! info "Added in 19.2" + +!!! info "Changed in 19.4" + Swapped ``--sendfile`` with ``--no-sendfile`` to actually allow + disabling. + +!!! info "Changed in 19.6" + added support for the ``SENDFILE`` environment variable + +### `reuse_port` + +**Command line:** `--reuse-port` + +**Default:** `False` + +Set the ``SO_REUSEPORT`` flag on the listening socket. + +!!! info "Added in 19.8" + +### `chdir` + +**Command line:** `--chdir` + +**Default:** + +``'.'`` + +Change directory to specified directory before loading apps. + +### `daemon` + +**Command line:** `-D`, `--daemon` + +**Default:** `False` + +Daemonize the Gunicorn process. + +Detaches the server from the controlling terminal and enters the +background. + +### `raw_env` + +**Command line:** `-e ENV`, `--env ENV` + +**Default:** `[]` + +Set environment variables in the execution environment. + +Should be a list of strings in the ``key=value`` format. + +For example on the command line: + +```console +$ gunicorn -b 127.0.0.1:8000 --env FOO=1 test:app +``` + +Or in the configuration file: + +```python +raw_env = ["FOO=1"] +``` + +### `pidfile` + +**Command line:** `-p FILE`, `--pid FILE` + +**Default:** `None` + +A filename to use for the PID file. + +If not set, no PID file will be written. + +### `worker_tmp_dir` + +**Command line:** `--worker-tmp-dir DIR` + +**Default:** `None` + +A directory to use for the worker heartbeat temporary file. + +If not set, the default temporary directory will be used. + +!!! note + The current heartbeat system involves calling ``os.fchmod`` on + temporary file handlers and may block a worker for arbitrary time + if the directory is on a disk-backed filesystem. + + See [blocking-os-fchmod](#blocking_os_fchmod) for more detailed information + and a solution for avoiding this problem. + +### `user` + +**Command line:** `-u USER`, `--user USER` + +**Default:** + +``os.geteuid()`` + +Switch worker processes to run as this user. + +A valid user id (as an integer) or the name of a user that can be +retrieved with a call to ``pwd.getpwnam(value)`` or ``None`` to not +change the worker process user. + +### `group` + +**Command line:** `-g GROUP`, `--group GROUP` + +**Default:** + +``os.getegid()`` + +Switch worker process to run as this group. + +A valid group id (as an integer) or the name of a user that can be +retrieved with a call to ``grp.getgrnam(value)`` or ``None`` to not +change the worker processes group. + +### `umask` + +**Command line:** `-m INT`, `--umask INT` + +**Default:** `0` + +A bit mask for the file mode on files written by Gunicorn. + +Note that this affects unix socket permissions. + +A valid value for the ``os.umask(mode)`` call or a string compatible +with ``int(value, 0)`` (``0`` means Python guesses the base, so values +like ``0``, ``0xFF``, ``0022`` are valid for decimal, hex, and octal +representations) + +### `initgroups` + +**Command line:** `--initgroups` + +**Default:** `False` + +If true, set the worker process's group access list with all of the +groups of which the specified username is a member, plus the specified +group id. + +!!! info "Added in 19.7" + +### `tmp_upload_dir` + +**Default:** `None` + +Directory to store temporary request data as they are read. + +This may disappear in the near future. + +This path should be writable by the process permissions set for Gunicorn +workers. If not specified, Gunicorn will choose a system generated +temporary directory. + +### `secure_scheme_headers` + +**Default:** `{'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}` + +A dictionary containing headers and values that the front-end proxy +uses to indicate HTTPS requests. If the source IP is permitted by +[forwarded-allow-ips](#forwarded_allow_ips) (below), *and* at least one request header matches +a key-value pair listed in this dictionary, then Gunicorn will set +``wsgi.url_scheme`` to ``https``, so your application can tell that the +request is secure. + +If the other headers listed in this dictionary are not present in the request, they will be ignored, +but if the other headers are present and do not match the provided values, then +the request will fail to parse. See the note below for more detailed examples of this behaviour. + +The dictionary should map upper-case header names to exact string +values. The value comparisons are case-sensitive, unlike the header +names, so make sure they're exactly what your front-end proxy sends +when handling HTTPS requests. + +It is important that your front-end proxy configuration ensures that +the headers defined here can not be passed directly from the client. + +### `forwarded_allow_ips` + +**Command line:** `--forwarded-allow-ips STRING` + +**Default:** `'127.0.0.1,::1'` + +Front-end's IPs from which allowed to handle set secure headers. +(comma separated). + +Set to ``*`` to disable checking of front-end IPs. This is useful for setups +where you don't know in advance the IP address of front-end, but +instead have ensured via other means that only your +authorized front-ends can access Gunicorn. + +By default, the value of the ``FORWARDED_ALLOW_IPS`` environment +variable. If it is not defined, the default is ``"127.0.0.1,::1"``. + +!!! note + This option does not affect UNIX socket connections. Connections not associated with + an IP address are treated as allowed, unconditionally. + +!!! note + The interplay between the request headers, the value of ``forwarded_allow_ips``, and the value of + ``secure_scheme_headers`` is complex. Various scenarios are documented below to further elaborate. + In each case, we have a request from the remote address 134.213.44.18, and the default value of + ``secure_scheme_headers``: + + .. code:: + + secure_scheme_headers = { + 'X-FORWARDED-PROTOCOL': 'ssl', + 'X-FORWARDED-PROTO': 'https', + 'X-FORWARDED-SSL': 'on' + } + + .. list-table:: + :header-rows: 1 + :align: center + :widths: auto + + * - ``forwarded-allow-ips`` + - Secure Request Headers + - Result + - Explanation + * - .. code:: + + ["127.0.0.1"] + - .. code:: + + X-Forwarded-Proto: https + - .. code:: + + wsgi.url_scheme = "http" + - IP address was not allowed + * - .. code:: + + "*" + - + - .. code:: + + wsgi.url_scheme = "http" + - IP address allowed, but no secure headers provided + * - .. code:: + + "*" + - .. code:: + + X-Forwarded-Proto: https + - .. code:: + + wsgi.url_scheme = "https" + - IP address allowed, one request header matched + * - .. code:: + + ["134.213.44.18"] + - .. code:: + + X-Forwarded-Ssl: on + X-Forwarded-Proto: http + - ``InvalidSchemeHeaders()`` raised + - IP address allowed, but the two secure headers disagreed on if HTTPS was used + +### `pythonpath` + +**Command line:** `--pythonpath STRING` + +**Default:** `None` + +A comma-separated list of directories to add to the Python path. + +e.g. +``'/home/djangoprojects/myproject,/home/python/mylibrary'``. + +### `paste` + +**Command line:** `--paste STRING`, `--paster STRING` + +**Default:** `None` + +Load a PasteDeploy config file. The argument may contain a ``#`` +symbol followed by the name of an app section from the config file, +e.g. ``production.ini#admin``. + +At this time, using alternate server blocks is not supported. Use the +command line arguments to control server configuration instead. + +### `proxy_protocol` + +**Command line:** `--proxy-protocol` + +**Default:** `False` + +Enable detect PROXY protocol (PROXY mode). + +Allow using HTTP and Proxy together. It may be useful for work with +stunnel as HTTPS frontend and Gunicorn as HTTP server. + +PROXY protocol: http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt + +Example for stunnel config:: + + [https] + protocol = proxy + accept = 443 + connect = 80 + cert = /etc/ssl/certs/stunnel.pem + key = /etc/ssl/certs/stunnel.key + +### `proxy_allow_ips` + +**Command line:** `--proxy-allow-from` + +**Default:** `'127.0.0.1,::1'` + +Front-end's IPs from which allowed accept proxy requests (comma separated). + +Set to ``*`` to disable checking of front-end IPs. This is useful for setups +where you don't know in advance the IP address of front-end, but +instead have ensured via other means that only your +authorized front-ends can access Gunicorn. + +!!! note + This option does not affect UNIX socket connections. Connections not associated with + an IP address are treated as allowed, unconditionally. + +### `protocol` + +**Command line:** `--protocol STRING` + +**Default:** `'http'` + +The protocol for incoming connections. + +* ``http`` - Standard HTTP/1.x (default) +* ``uwsgi`` - uWSGI binary protocol (for nginx uwsgi_pass) + +When using the uWSGI protocol, Gunicorn can receive requests from +nginx using the uwsgi_pass directive:: + + upstream gunicorn { + server 127.0.0.1:8000; + } + location / { + uwsgi_pass gunicorn; + include uwsgi_params; + } + +### `uwsgi_allow_ips` + +**Command line:** `--uwsgi-allow-from` + +**Default:** `'127.0.0.1,::1'` + +IPs allowed to send uWSGI protocol requests (comma separated). + +Set to ``*`` to allow all IPs. This is useful for setups where you +don't know in advance the IP address of front-end, but instead have +ensured via other means that only your authorized front-ends can +access Gunicorn. + +!!! note + This option does not affect UNIX socket connections. Connections not associated with + an IP address are treated as allowed, unconditionally. + +### `raw_paste_global_conf` + +**Command line:** `--paste-global CONF` + +**Default:** `[]` + +Set a PasteDeploy global config variable in ``key=value`` form. + +The option can be specified multiple times. + +The variables are passed to the PasteDeploy entrypoint. Example:: + + $ gunicorn -b 127.0.0.1:8000 --paste development.ini --paste-global FOO=1 --paste-global BAR=2 + +!!! info "Added in 19.7" + +### `permit_obsolete_folding` + +**Command line:** `--permit-obsolete-folding` + +**Default:** `False` + +Permit requests employing obsolete HTTP line folding mechanism + +The folding mechanism was deprecated by rfc7230 Section 3.2.4 and will not be + employed in HTTP request headers from standards-compliant HTTP clients. + +This option is provided to diagnose backwards-incompatible changes. +Use with care and only if necessary. Temporary; the precise effect of this option may +change in a future version, or it may be removed altogether. + +!!! info "Added in 23.0.0" + +### `strip_header_spaces` + +**Command line:** `--strip-header-spaces` + +**Default:** `False` + +Strip spaces present between the header name and the the ``:``. + +This is known to induce vulnerabilities and is not compliant with the HTTP/1.1 standard. +See https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn. + +Use with care and only if necessary. Deprecated; scheduled for removal in 25.0.0 + +!!! info "Added in 20.0.1" + +### `permit_unconventional_http_method` + +**Command line:** `--permit-unconventional-http-method` + +**Default:** `False` + +Permit HTTP methods not matching conventions, such as IANA registration guidelines + +This permits request methods of length less than 3 or more than 20, +methods with lowercase characters or methods containing the # character. +HTTP methods are case sensitive by definition, and merely uppercase by convention. + +If unset, Gunicorn will apply nonstandard restrictions and cause 400 response status +in cases where otherwise 501 status is expected. While this option does modify that +behaviour, it should not be depended upon to guarantee standards-compliant behaviour. +Rather, it is provided temporarily, to assist in diagnosing backwards-incompatible +changes around the incomplete application of those restrictions. + +Use with care and only if necessary. Temporary; scheduled for removal in 24.0.0 + +!!! info "Added in 22.0.0" + +### `permit_unconventional_http_version` + +**Command line:** `--permit-unconventional-http-version` + +**Default:** `False` + +Permit HTTP version not matching conventions of 2023 + +This disables the refusal of likely malformed request lines. +It is unusual to specify HTTP 1 versions other than 1.0 and 1.1. + +This option is provided to diagnose backwards-incompatible changes. +Use with care and only if necessary. Temporary; the precise effect of this option may +change in a future version, or it may be removed altogether. + +!!! info "Added in 22.0.0" + +### `casefold_http_method` + +**Command line:** `--casefold-http-method` + +**Default:** `False` + +Transform received HTTP methods to uppercase + +HTTP methods are case sensitive by definition, and merely uppercase by convention. + +This option is provided because previous versions of gunicorn defaulted to this behaviour. + +Use with care and only if necessary. Deprecated; scheduled for removal in 24.0.0 + +!!! info "Added in 22.0.0" + +### `forwarder_headers` + +**Command line:** `--forwarder-headers` + +**Default:** `'SCRIPT_NAME,PATH_INFO'` + +A list containing upper-case header field names that the front-end proxy +(see [forwarded-allow-ips](#forwarded_allow_ips)) sets, to be used in WSGI environment. + +This option has no effect for headers not present in the request. + +This option can be used to transfer ``SCRIPT_NAME``, ``PATH_INFO`` +and ``REMOTE_USER``. + +It is important that your front-end proxy configuration ensures that +the headers defined here can not be passed directly from the client. + +### `header_map` + +**Command line:** `--header-map` + +**Default:** `'drop'` + +Configure how header field names are mapped into environ + +Headers containing underscores are permitted by RFC9110, +but gunicorn joining headers of different names into +the same environment variable will dangerously confuse applications as to which is which. + +The safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped. +The value ``refuse`` will return an error if a request contains *any* such header. +The value ``dangerous`` matches the previous, not advisable, behaviour of mapping different +header field names into the same environ name. + +If the source is permitted as explained in [forwarded-allow-ips](#forwarded_allow_ips), *and* the header name is +present in [forwarder-headers](#forwarder_headers), the header is mapped into environment regardless of +the state of this setting. + +Use with care and only if necessary and after considering if your problem could +instead be solved by specifically renaming or rewriting only the intended headers +on a proxy in front of Gunicorn. + +!!! info "Added in 22.0.0" + +### `root_path` + +**Command line:** `--root-path STRING` + +**Default:** `''` + +The root path for ASGI applications. + +This is used to set the ``root_path`` in the ASGI scope, which +allows applications to know their mount point when behind a +reverse proxy. + +For example, if your application is mounted at ``/api``, set +this to ``/api``. + +!!! info "Added in 24.0.0" + +## Server Socket + +### `bind` + +**Command line:** `-b ADDRESS`, `--bind ADDRESS` + +**Default:** `['127.0.0.1:8000']` + +The socket to bind. + +A string of the form: ``HOST``, ``HOST:PORT``, ``unix:PATH``, +``fd://FD``. An IP is a valid ``HOST``. + +!!! info "Changed in 20.0" + Support for ``fd://FD`` got added. + +Multiple addresses can be bound. ex.:: + + $ gunicorn -b 127.0.0.1:8000 -b [::1]:8000 test:app + +will bind the `test:app` application on localhost both on ipv6 +and ipv4 interfaces. + +If the ``PORT`` environment variable is defined, the default +is ``['0.0.0.0:$PORT']``. If it is not defined, the default +is ``['127.0.0.1:8000']``. + +### `backlog` + +**Command line:** `--backlog INT` + +**Default:** `2048` + +The maximum number of pending connections. + +This refers to the number of clients that can be waiting to be served. +Exceeding this number results in the client getting an error when +attempting to connect. It should only affect servers under significant +load. + +Must be a positive integer. Generally set in the 64-2048 range. + +## Worker Processes + +### `workers` + +**Command line:** `-w INT`, `--workers INT` + +**Default:** `1` + +The number of worker processes for handling requests. + +A positive integer generally in the ``2-4 x $(NUM_CORES)`` range. +You'll want to vary this a bit to find the best for your particular +application's work load. + +By default, the value of the ``WEB_CONCURRENCY`` environment variable, +which is set by some Platform-as-a-Service providers such as Heroku. If +it is not defined, the default is ``1``. + +### `worker_class` + +**Command line:** `-k STRING`, `--worker-class STRING` + +**Default:** `'sync'` + +The type of workers to use. + +The default class (``sync``) should handle most "normal" types of +workloads. You'll want to read :doc:`design` for information on when +you might want to choose one of the other worker classes. Required +libraries may be installed using setuptools' ``extras_require`` feature. + +A string referring to one of the following bundled classes: + +* ``sync`` +* ``eventlet`` - Requires eventlet >= 0.24.1 (or install it via + ``pip install gunicorn[eventlet]``) +* ``gevent`` - Requires gevent >= 1.4 (or install it via + ``pip install gunicorn[gevent]``) +* ``tornado`` - Requires tornado >= 0.2 (or install it via + ``pip install gunicorn[tornado]``) +* ``gthread`` - Python 2 requires the futures package to be installed + (or install it via ``pip install gunicorn[gthread]``) + +Optionally, you can provide your own worker by giving Gunicorn a +Python path to a subclass of ``gunicorn.workers.base.Worker``. +This alternative syntax will load the gevent class: +``gunicorn.workers.ggevent.GeventWorker``. + +### `threads` + +**Command line:** `--threads INT` + +**Default:** `1` + +The number of worker threads for handling requests. + +Run each worker with the specified number of threads. + +A positive integer generally in the ``2-4 x $(NUM_CORES)`` range. +You'll want to vary this a bit to find the best for your particular +application's work load. + +If it is not defined, the default is ``1``. + +This setting only affects the Gthread worker type. + +!!! note + If you try to use the ``sync`` worker type and set the ``threads`` + setting to more than 1, the ``gthread`` worker type will be used + instead. + +### `worker_connections` + +**Command line:** `--worker-connections INT` + +**Default:** `1000` + +The maximum number of simultaneous clients. + +This setting only affects the ``gthread``, ``eventlet`` and ``gevent`` worker types. + +### `max_requests` + +**Command line:** `--max-requests INT` + +**Default:** `0` + +The maximum number of requests a worker will process before restarting. + +Any value greater than zero will limit the number of requests a worker +will process before automatically restarting. This is a simple method +to help limit the damage of memory leaks. + +If this is set to zero (the default) then the automatic worker +restarts are disabled. + +### `max_requests_jitter` + +**Command line:** `--max-requests-jitter INT` + +**Default:** `0` + +The maximum jitter to add to the *max_requests* setting. + +The jitter causes the restart per worker to be randomized by +``randint(0, max_requests_jitter)``. This is intended to stagger worker +restarts to avoid all workers restarting at the same time. + +!!! info "Added in 19.2" + +### `timeout` + +**Command line:** `-t INT`, `--timeout INT` + +**Default:** `30` + +Workers silent for more than this many seconds are killed and restarted. + +Value is a positive number or 0. Setting it to 0 has the effect of +infinite timeouts by disabling timeouts for all workers entirely. + +Generally, the default of thirty seconds should suffice. Only set this +noticeably higher if you're sure of the repercussions for sync workers. +For the non sync workers it just means that the worker process is still +communicating and is not tied to the length of time required to handle a +single request. + +### `graceful_timeout` + +**Command line:** `--graceful-timeout INT` + +**Default:** `30` + +Timeout for graceful workers restart in seconds. + +After receiving a restart signal, workers have this much time to finish +serving requests. Workers still alive after the timeout (starting from +the receipt of the restart signal) are force killed. + +### `keepalive` + +**Command line:** `--keep-alive INT` + +**Default:** `2` + +The number of seconds to wait for requests on a Keep-Alive connection. + +Generally set in the 1-5 seconds range for servers with direct connection +to the client (e.g. when you don't have separate load balancer). When +Gunicorn is deployed behind a load balancer, it often makes sense to +set this to a higher value. + +!!! note + ``sync`` worker does not support persistent connections and will + ignore this option. + +### `asgi_loop` + +**Command line:** `--asgi-loop STRING` + +**Default:** `'auto'` + +Event loop implementation for ASGI workers. + +- auto: Use uvloop if available, otherwise asyncio +- asyncio: Use Python's built-in asyncio event loop +- uvloop: Use uvloop (must be installed separately) + +This setting only affects the ``asgi`` worker type. + +uvloop typically provides better performance but requires +installing the uvloop package. + +!!! info "Added in 24.0.0" + +### `asgi_lifespan` + +**Command line:** `--asgi-lifespan STRING` + +**Default:** `'auto'` + +Control ASGI lifespan protocol handling. + +- auto: Detect if app supports lifespan, enable if so +- on: Always run lifespan protocol (fail if unsupported) +- off: Never run lifespan protocol + +The lifespan protocol allows ASGI applications to run code at +startup and shutdown. This is essential for frameworks like +FastAPI that need to initialize database connections, caches, +or other resources. + +This setting only affects the ``asgi`` worker type. + +!!! info "Added in 24.0.0" diff --git a/docs/content/run.md b/docs/content/run.md new file mode 100644 index 00000000..a727c1c4 --- /dev/null +++ b/docs/content/run.md @@ -0,0 +1,154 @@ +# Running Gunicorn + +You can run Gunicorn directly from the command line or integrate it with +popular frameworks like Django, Pyramid, or TurboGears. For deployment +patterns see the [deployment guide](deploy.md). + +## Commands + +After installation you have access to the `gunicorn` executable. + + +### `gunicorn` + +Basic usage: + +```bash +gunicorn [OPTIONS] [WSGI_APP] +``` + +`WSGI_APP` follows the pattern `MODULE_NAME:VARIABLE_NAME`. The module can be a +full dotted path. The variable refers to a WSGI callable defined in that +module. + +!!! info "Changed in 20.1.0" + `WSGI_APP` can be omitted when defined in a [configuration file](configure.md). + + + +Example test application: + +```python +def app(environ, start_response): + """Simplest possible application object""" + data = b"Hello, World!\n" + status = "200 OK" + response_headers = [ + ("Content-type", "text/plain"), + ("Content-Length", str(len(data.md))) + ] + start_response(status, response_headers) + return iter([data]) +``` + +Run it with: + +```bash +gunicorn --workers=2 test:app +``` + +You can also expose a factory function that returns the application: + +```python +def create_app(): + app = FrameworkApp() + ... + return app +``` + +```bash +gunicorn --workers=2 'test:create_app()' +``` + +Passing positional and keyword arguments is supported but prefer +configuration files or environment variables for anything beyond quick tests. + +#### Commonly used arguments + +- `-c CONFIG`, `--config CONFIG` — configuration file (`PATH`, `file:PATH`, or + `python:MODULE_NAME`). +- `-b BIND`, `--bind BIND` — socket to bind (host, host:port, `fd://FD`, + or `unix:PATH`). +- `-w WORKERS`, `--workers WORKERS` — number of worker processes, typically + two to four per CPU core. See the [FAQ](faq.md) for tuning tips. +- `-k WORKERCLASS`, `--worker-class WORKERCLASS` — worker type (`sync`, + `eventlet`, `gevent`, `tornado`, `gthread`). Read the + [settings entry](reference/settings.md#worker_class) before switching classes. +- `-n APP_NAME`, `--name APP_NAME` — set the process name (requires + [`setproctitle`](https://pypi.python.org/pypi/setproctitle)). + +You can pass any setting via the environment variable +`GUNICORN_CMD_ARGS`. See the [configuration guide](configure.md) and +[settings reference](reference/settings.md) for details. + +## Integration + +Gunicorn integrates cleanly with Django and Paste Deploy applications. + +### Django + +Gunicorn looks for a WSGI callable named `application`. A typical invocation is: + +```bash +gunicorn myproject.wsgi +``` + +!!! note + Ensure your project is on `PYTHONPATH`. The easiest way is to run this command + from the directory containing `manage.py`. + + + +Set environment variables with `--env` and add your project to `PYTHONPATH` +if needed: + +```bash +gunicorn --env DJANGO_SETTINGS_MODULE=myproject.settings myproject.wsgi +``` + +See [`raw_env`](reference/settings.md#raw_env) and [`pythonpath`](reference/settings.md#pythonpath) for +more options. + +### Paste Deployment + +Frameworks such as Pyramid and TurboGears often rely on Paste Deployment +configuration. You can use Gunicorn in two ways. + +#### As a Paste server runner + +Let your framework command (for example `pserve` or `gearbox`) load Gunicorn by +configuring it as the server: + +```ini +[server:main] +use = egg:gunicorn#main +host = 127.0.0.1 +port = 8080 +workers = 3 +``` + +This approach is quick to set up but Gunicorn cannot control how the +application loads. Options like [`reload`](reference/settings.md#reload) will be ignored and +hot upgrades are unavailable. Features such as daemon mode may conflict with +what your framework already provides. Prefer running those features through the +framework (for example `pserve --reload`). Advanced configuration is still +possible by pointing the `config` key at a Gunicorn configuration file. + +#### Using Gunicorn's Paste support + +Use the [`paste`](reference/settings.md#paste) option to load a Paste configuration directly +with the Gunicorn CLI. This unlocks Gunicorn's reloader and hot code upgrades, +while still letting Paste define the application object. + +```bash +gunicorn --paste development.ini -b :8080 --chdir /path/to/project +``` + +Select a different application section by appending the name: + +```bash +gunicorn --paste development.ini#admin -b :8080 --chdir /path/to/project +``` + +In both modes Gunicorn will honor any Paste `loggers` configuration unless you +override it with Gunicorn-specific [logging settings](reference/settings.md#logging). diff --git a/docs/content/signals.md b/docs/content/signals.md new file mode 100644 index 00000000..ce08ca09 --- /dev/null +++ b/docs/content/signals.md @@ -0,0 +1,97 @@ + +# Signal Handling + +A quick reference to the signals handled by Gunicorn. This includes the signals +used internally to coordinate with worker processes. + +## Master process + +- `QUIT`, `INT` — quick shutdown. +- `TERM` — graceful shutdown; waits for workers to finish requests up to + [`graceful_timeout`](reference/settings.md#graceful_timeout). +- `HUP` — reload configuration, spawn new workers, and gracefully stop old + ones. If the app is not preloaded (see [`preload_app`](reference/settings.md#preload_app)) + the application code is reloaded too. +- `TTIN` — increase worker count by one. +- `TTOU` — decrease worker count by one. +- `USR1` — reopen log files. +- `USR2` — perform a binary upgrade. Send `TERM` to the old master afterwards + to stop it. This also reloads preloaded applications (see + [binary upgrades](#binary-upgrade)). +- `WINCH` — gracefully stop workers when Gunicorn runs as a daemon. + +## Worker process + +Workers rarely need direct signalling—if the master stays alive it will respawn +workers automatically. + +- `QUIT`, `INT` — quick shutdown. +- `TERM` — graceful shutdown. +- `USR1` — reopen log files. + +## Reload the configuration + +Use `HUP` to reload Gunicorn on the fly: + +```text +2013-06-29 06:26:55 [20682] [INFO] Handling signal: hup +2013-06-29 06:26:55 [20682] [INFO] Hang up: Master +2013-06-29 06:26:55 [20703] [INFO] Booting worker with pid: 20703 +2013-06-29 06:26:55 [20702] [INFO] Booting worker with pid: 20702 +2013-06-29 06:26:55 [20688] [INFO] Worker exiting (pid: 20688) +2013-06-29 06:26:55 [20687] [INFO] Worker exiting (pid: 20687) +2013-06-29 06:26:55 [20689] [INFO] Worker exiting (pid: 20689) +2013-06-29 06:26:55 [20704] [INFO] Booting worker with pid: 20704 +``` + +Gunicorn reloads its settings, starts new workers, and gracefully shuts down the +previous ones. If the app is not preloaded it reloads the application module as +well. + + +## Upgrading to a new binary on the fly + +!!! info "Changed in 19.6.0" + PID files now follow the pattern `.pid.2` instead of `.pid.oldbin`. + + + +You can replace the Gunicorn binary without downtime. Incoming requests remain +served and preloaded applications reload. + +1. Replace the old binary and send `USR2` to the master. Gunicorn starts a new + master whose PID file ends with `.2` and spawns new workers. + + ```text + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 20844 benoitc 20 0 54808 11m 3352 S 0.0 0.1 0:00.36 gunicorn: master [test:app] + 20849 benoitc 20 0 54808 9.9m 1500 S 0.0 0.1 0:00.02 gunicorn: worker [test:app] + 20850 benoitc 20 0 54808 9.9m 1500 S 0.0 0.1 0:00.01 gunicorn: worker [test:app] + 20851 benoitc 20 0 54808 9.9m 1500 S 0.0 0.1 0:00.01 gunicorn: worker [test:app] + 20854 benoitc 20 0 55748 12m 3348 S 0.0 0.2 0:00.35 gunicorn: master [test:app] + 20859 benoitc 20 0 55748 11m 1500 S 0.0 0.1 0:00.01 gunicorn: worker [test:app] + 20860 benoitc 20 0 55748 11m 1500 S 0.0 0.1 0:00.00 gunicorn: worker [test:app] + 20861 benoitc 20 0 55748 11m 1500 S 0.0 0.1 0:00.01 gunicorn: worker [test:app] + ``` + +2. Send `WINCH` to the old master to gracefully stop its workers. + +You can still roll back while the old master keeps its listen sockets: + +1. Send `HUP` to the old master to restart its workers without reloading the + config file. +2. Send `TERM` to the new master to shut down its workers gracefully. +3. Send `QUIT` to the new master to force it to exit. + +If the new workers linger, send `KILL` after the new master quits. + +To complete the upgrade, send `TERM` to the old master so only the new server +continues running: + +```text +PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND +20854 benoitc 20 0 55748 12m 3348 S 0.0 0.2 0:00.45 gunicorn: master [test:app] +20859 benoitc 20 0 55748 11m 1500 S 0.0 0.1 0:00.02 gunicorn: worker [test:app] +20860 benoitc 20 0 55748 11m 1500 S 0.0 0.1 0:00.02 gunicorn: worker [test:app] +20861 benoitc 20 0 55748 11m 1500 S 0.0 0.1 0:00.01 gunicorn: worker [test:app] +``` diff --git a/docs/content/styles/overrides.css b/docs/content/styles/overrides.css new file mode 100644 index 00000000..f4b328e8 --- /dev/null +++ b/docs/content/styles/overrides.css @@ -0,0 +1,504 @@ +/* Gunicorn Punchy Theme */ +:root { + --gunicorn-green: #00a650; + --gunicorn-green-dark: #008542; + --gunicorn-green-light: #00c853; + --gunicorn-teal: #00bfa5; + --gunicorn-bg: #fafafa; + --gunicorn-card: #ffffff; + + --md-primary-fg-color: var(--gunicorn-green); + --md-primary-fg-color--light: var(--gunicorn-green-light); + --md-primary-fg-color--dark: var(--gunicorn-green-dark); + --md-accent-fg-color: var(--gunicorn-teal); + --md-typeset-a-color: var(--gunicorn-green); +} + +[data-md-color-scheme="slate"] { + --gunicorn-bg: #0d1117; + --gunicorn-card: #161b22; + --md-default-bg-color: #0d1117; + --md-default-bg-color--light: #161b22; +} + +/* Header - punchy gradient */ +.md-header { + background: linear-gradient(135deg, var(--gunicorn-green-dark) 0%, var(--gunicorn-green) 100%); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.md-tabs { + background: linear-gradient(135deg, var(--gunicorn-green) 0%, var(--gunicorn-green-light) 100%); +} + +/* Logo bigger */ +.md-header__button.md-logo img, +.md-header__button.md-logo svg { + height: 2rem; +} + +/* Navigation styling */ +.md-nav__link:hover { + color: var(--gunicorn-green); +} + +.md-nav__link--active { + color: var(--gunicorn-green); + font-weight: 600; +} + +/* Code blocks - punchy */ +.md-typeset code { + background: rgba(0, 166, 80, 0.08); + color: var(--gunicorn-green-dark); + border-radius: 4px; +} + +[data-md-color-scheme="slate"] .md-typeset code { + background: rgba(0, 200, 83, 0.12); + color: var(--gunicorn-green-light); +} + +.md-typeset pre { + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +[data-md-color-scheme="slate"] .md-typeset pre { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); +} + +/* Admonitions - punchy colors */ +.md-typeset .admonition, +.md-typeset details { + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.md-typeset .admonition.note, +.md-typeset details.note { + border-color: var(--gunicorn-teal); +} + +.md-typeset .note > .admonition-title, +.md-typeset .note > summary { + background-color: rgba(0, 191, 165, 0.1); +} + +.md-typeset .admonition.tip, +.md-typeset details.tip { + border-color: var(--gunicorn-green); +} + +.md-typeset .tip > .admonition-title, +.md-typeset .tip > summary { + background-color: rgba(0, 166, 80, 0.1); +} + +/* Tables - cleaner */ +.md-typeset table:not([class]) { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.md-typeset table:not([class]) th { + background: var(--gunicorn-green); + color: white; + font-weight: 600; +} + +/* Buttons - punchy */ +.md-typeset .md-button { + border-radius: 8px; + font-weight: 600; + text-transform: none; + letter-spacing: 0; + transition: all 0.2s ease; +} + +.md-typeset .md-button--primary { + background: linear-gradient(135deg, var(--gunicorn-green) 0%, var(--gunicorn-green-light) 100%); + border: none; + box-shadow: 0 4px 12px rgba(0, 166, 80, 0.3); +} + +.md-typeset .md-button--primary:hover { + box-shadow: 0 6px 20px rgba(0, 166, 80, 0.4); + transform: translateY(-2px); +} + +/* Search */ +.md-search__form { + border-radius: 8px; +} + +/* Footer */ +.md-footer { + background: linear-gradient(135deg, var(--gunicorn-green-dark) 0%, #1a1a2e 100%); +} + +.md-footer-meta { + background: rgba(0, 0, 0, 0.2); +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-thumb { + background: var(--gunicorn-green); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--gunicorn-green-light); +} + +/* Selection */ +::selection { + background: rgba(0, 166, 80, 0.3); +} + +/* ================================ + Homepage Specific Styles + ================================ */ + +/* These are for the non-custom template pages */ +.md-typeset .hero { + margin: 2rem 0 3rem; + padding: 3.5rem; + background: linear-gradient(135deg, var(--gunicorn-green-dark) 0%, var(--gunicorn-green) 50%, var(--gunicorn-teal) 100%); + color: #fff; + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0, 166, 80, 0.25); +} + +[data-md-color-scheme="slate"] .md-typeset .hero { + background: linear-gradient(135deg, #0d1117 0%, var(--gunicorn-green-dark) 50%, var(--gunicorn-green) 100%); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); +} + +.md-typeset .hero__inner { + display: flex; + flex-wrap: wrap; + gap: 2.5rem; + align-items: center; + justify-content: space-between; +} + +.md-typeset .hero__copy { + flex: 1 1 320px; + max-width: 520px; + font-size: 1.05rem; + line-height: 1.6; +} + +.md-typeset .hero__copy h1 { + margin: 0 0 1rem; + font-size: 2.6rem; + font-weight: 800; + line-height: 1.15; + letter-spacing: -0.02em; +} + +.md-typeset .hero__tagline { + font-size: 1.15rem; + opacity: 0.95; + margin-bottom: 0; +} + +.md-typeset .hero__cta { + margin-top: 2rem; + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.md-typeset .hero__code { + flex: 1 1 260px; + max-width: 400px; + background: rgba(0, 0, 0, 0.25); + border-radius: 12px; + padding: 1.5rem; + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.md-typeset .hero__code pre { + margin: 0 0 1rem; + border: none; + background: rgba(0, 0, 0, 0.4); + color: #e8f5ea; + box-shadow: none; +} + +.md-typeset .hero__version { + font-weight: 700; + font-size: 0.9rem; + opacity: 0.9; +} + +.md-typeset .hero__logo { + height: 72px; + margin-bottom: 1.5rem; + filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.2)); +} + +/* Pillars */ +.md-typeset .pillars { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 2rem; + margin: 3rem 0; +} + +.md-typeset .pillar { + text-align: center; + padding: 2rem; + background: var(--gunicorn-card); + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +[data-md-color-scheme="slate"] .md-typeset .pillar { + background: var(--gunicorn-card); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.md-typeset .pillar:hover { + transform: translateY(-4px); + box-shadow: 0 12px 32px rgba(0, 166, 80, 0.15); +} + +.md-typeset .pillar__icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.md-typeset .pillar h3 { + margin: 0 0 0.5rem; + font-size: 1.3rem; + font-weight: 700; + color: var(--gunicorn-green-dark); +} + +[data-md-color-scheme="slate"] .md-typeset .pillar h3 { + color: var(--gunicorn-green-light); +} + +.md-typeset .pillar p { + margin: 0; + font-size: 0.95rem; + opacity: 0.8; +} + +/* Frameworks */ +.md-typeset .frameworks { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + margin: 2rem 0 3rem; +} + +.md-typeset .framework { + background: var(--gunicorn-card); + border: 2px solid transparent; + border-radius: 50px; + padding: 0.75rem 1.75rem; + font-weight: 600; + font-size: 0.95rem; + color: var(--gunicorn-green-dark); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + transition: all 0.2s ease; +} + +[data-md-color-scheme="slate"] .md-typeset .framework { + background: var(--gunicorn-card); + color: #e8f5ea; +} + +.md-typeset .framework:hover { + border-color: var(--gunicorn-green); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 166, 80, 0.2); +} + +/* Feature Grid */ +.md-typeset .feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1.5rem; + margin: 2.5rem 0 3rem; +} + +.md-typeset .feature-card { + background: var(--gunicorn-card); + border-radius: 12px; + padding: 1.75rem; + border: 1px solid rgba(0, 166, 80, 0.1); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06); + transition: all 0.2s ease; +} + +[data-md-color-scheme="slate"] .md-typeset .feature-card { + background: var(--gunicorn-card); + border-color: rgba(0, 200, 83, 0.15); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.md-typeset .feature-card:hover { + transform: translateY(-4px); + border-color: var(--gunicorn-green); + box-shadow: 0 12px 32px rgba(0, 166, 80, 0.15); +} + +.md-typeset .feature-card h3 { + margin-top: 0; + font-size: 1.2rem; + font-weight: 700; + color: var(--gunicorn-green-dark); + display: flex; + align-items: center; + gap: 0.5rem; +} + +[data-md-color-scheme="slate"] .md-typeset .feature-card h3 { + color: var(--gunicorn-green-light); +} + +.md-typeset .feature-card p { + font-size: 0.95rem; + opacity: 0.8; + margin-bottom: 1rem; +} + +.md-typeset .feature-card a { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-weight: 600; + color: var(--gunicorn-green); +} + +.md-typeset .feature-card a:hover { + color: var(--gunicorn-green-light); +} + +/* Badge */ +.md-typeset .badge { + display: inline-block; + font-size: 0.65rem; + font-weight: 700; + text-transform: uppercase; + padding: 0.2rem 0.6rem; + border-radius: 50px; + vertical-align: middle; + letter-spacing: 0.05em; +} + +.md-typeset .badge--new { + background: linear-gradient(135deg, var(--gunicorn-green) 0%, var(--gunicorn-teal) 100%); + color: #fff; +} + +/* Quick Links */ +.md-typeset .quick-links { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin: 2rem 0; +} + +.md-typeset .quick-link { + display: block; + padding: 1.5rem; + background: var(--gunicorn-card); + border-radius: 12px; + border: 2px solid transparent; + text-decoration: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); + transition: all 0.2s ease; +} + +[data-md-color-scheme="slate"] .md-typeset .quick-link { + background: var(--gunicorn-card); +} + +.md-typeset .quick-link:hover { + border-color: var(--gunicorn-green); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 166, 80, 0.15); +} + +.md-typeset .quick-link strong { + display: block; + font-size: 1.1rem; + font-weight: 700; + color: var(--gunicorn-green-dark); + margin-bottom: 0.25rem; +} + +[data-md-color-scheme="slate"] .md-typeset .quick-link strong { + color: var(--gunicorn-green-light); +} + +.md-typeset .quick-link span { + font-size: 0.9rem; + opacity: 0.7; +} + +/* Community Links */ +.md-typeset .community-links { + margin: 1.5rem 0; +} + +.md-typeset .community-links ul { + list-style: none; + padding: 0; + margin: 0; +} + +.md-typeset .community-links li { + margin-bottom: 0.75rem; +} + +/* Footer */ +.md-footer-meta__inner { + flex-wrap: wrap; +} + +/* Responsive */ +@media (max-width: 960px) { + .md-typeset .hero { + padding: 2.5rem; + } + + .md-typeset .hero__copy h1 { + font-size: 2rem; + } +} + +@media (max-width: 720px) { + .md-typeset .hero { + margin-top: 1.5rem; + padding: 2rem; + } + + .md-typeset .hero__cta { + flex-direction: column; + align-items: stretch; + } + + .md-typeset .hero__code { + width: 100%; + } + + .md-typeset .pillars { + grid-template-columns: 1fr; + } +} diff --git a/docs/content/uwsgi.md b/docs/content/uwsgi.md new file mode 100644 index 00000000..af2b0c75 --- /dev/null +++ b/docs/content/uwsgi.md @@ -0,0 +1,266 @@ +# uWSGI Protocol + +Gunicorn supports the uWSGI binary protocol, allowing it to receive requests from +nginx using the `uwsgi_pass` directive. This provides efficient communication +between nginx and Gunicorn without HTTP overhead. + +!!! note + This is the **uWSGI binary protocol**, not the uWSGI server. Gunicorn + implements the protocol to receive requests from nginx, similar to how + the uWSGI server would. + +## Quick Start + +Enable uWSGI protocol support: + +```bash +gunicorn myapp:app --protocol uwsgi --bind 127.0.0.1:8000 +``` + +Configure nginx to forward requests: + +```nginx +upstream gunicorn { + server 127.0.0.1:8000; +} + +server { + listen 80; + server_name example.com; + + location / { + uwsgi_pass gunicorn; + include uwsgi_params; + } +} +``` + +## Why Use uWSGI Protocol? + +The uWSGI binary protocol offers several advantages over HTTP proxying: + +- **Lower overhead** - Binary format is more compact than HTTP headers +- **Better integration** - nginx's native uwsgi module is highly optimized +- **Simpler configuration** - No need to reconstruct HTTP headers + +## Configuration + +### Protocol Setting + +Switch from HTTP to uWSGI protocol: + +```bash +gunicorn myapp:app --protocol uwsgi +``` + +Or in a configuration file: + +```python +# gunicorn.conf.py +protocol = "uwsgi" +``` + +### Allowed IPs + +By default, uWSGI protocol requests are only accepted from localhost +(`127.0.0.1` and `::1`). This prevents unauthorized hosts from sending +requests directly to Gunicorn. + +To allow additional IPs: + +```bash +gunicorn myapp:app --protocol uwsgi --uwsgi-allow-from 10.0.0.1,10.0.0.2 +``` + +To allow all IPs (not recommended for production): + +```bash +gunicorn myapp:app --protocol uwsgi --uwsgi-allow-from '*' +``` + +!!! warning + Only allow IPs from trusted sources. The uWSGI protocol does not provide + authentication, so anyone who can connect can send requests. + +!!! note + UNIX socket connections are always allowed regardless of this setting. + +### Using UNIX Sockets + +For better performance and security, use UNIX sockets instead of TCP: + +```bash +gunicorn myapp:app --protocol uwsgi --bind unix:/run/gunicorn.sock +``` + +Nginx configuration: + +```nginx +upstream gunicorn { + server unix:/run/gunicorn.sock; +} + +server { + listen 80; + + location / { + uwsgi_pass gunicorn; + include uwsgi_params; + } +} +``` + +## Nginx Configuration + +### Basic Setup + +Create or verify the `uwsgi_params` file exists (usually at `/etc/nginx/uwsgi_params`): + +```nginx +uwsgi_param QUERY_STRING $query_string; +uwsgi_param REQUEST_METHOD $request_method; +uwsgi_param CONTENT_TYPE $content_type; +uwsgi_param CONTENT_LENGTH $content_length; + +uwsgi_param REQUEST_URI $request_uri; +uwsgi_param PATH_INFO $document_uri; +uwsgi_param DOCUMENT_ROOT $document_root; +uwsgi_param SERVER_PROTOCOL $server_protocol; +uwsgi_param REQUEST_SCHEME $scheme; +uwsgi_param HTTPS $https if_not_empty; + +uwsgi_param REMOTE_ADDR $remote_addr; +uwsgi_param REMOTE_PORT $remote_port; +uwsgi_param SERVER_PORT $server_port; +uwsgi_param SERVER_NAME $server_name; +``` + +### With SSL Termination + +When nginx handles SSL and forwards to Gunicorn: + +```nginx +server { + listen 443 ssl; + server_name example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + uwsgi_pass gunicorn; + include uwsgi_params; + uwsgi_param HTTPS on; + } +} +``` + +### Load Balancing + +Distribute requests across multiple Gunicorn instances: + +```nginx +upstream gunicorn { + least_conn; + server 127.0.0.1:8000; + server 127.0.0.1:8001; + server 127.0.0.1:8002; +} + +server { + listen 80; + + location / { + uwsgi_pass gunicorn; + include uwsgi_params; + } +} +``` + +### Static Files + +Serve static files directly from nginx: + +```nginx +server { + listen 80; + + location /static/ { + alias /path/to/static/; + } + + location / { + uwsgi_pass gunicorn; + include uwsgi_params; + } +} +``` + +## Protocol Details + +The uWSGI protocol uses a compact binary format: + +| Bytes | Field | Description | +|-------|-------|-------------| +| 0 | modifier1 | Packet type (0 = WSGI request) | +| 1-2 | datasize | Size of vars block (little-endian) | +| 3 | modifier2 | Additional flags (usually 0) | + +After the header, the vars block contains CGI-style key-value pairs: + +``` +[2-byte key_size][key][2-byte val_size][value]... +``` + +Standard CGI variables like `REQUEST_METHOD`, `PATH_INFO`, and `QUERY_STRING` +are extracted from this block to construct the WSGI environ. + +## Combining with HTTP + +You can run Gunicorn with both HTTP and uWSGI protocol support by running +separate instances: + +```bash +# HTTP for direct access +gunicorn myapp:app --bind 127.0.0.1:8080 + +# uWSGI for nginx +gunicorn myapp:app --protocol uwsgi --bind 127.0.0.1:8000 +``` + +## Troubleshooting + +### ForbiddenUWSGIRequest Error + +If you see "Forbidden uWSGI request from IP", the connecting IP is not in +the allowed list. Either: + +1. Add the IP to `--uwsgi-allow-from` +2. Use UNIX sockets instead +3. Ensure nginx is connecting from an allowed IP + +### Invalid uWSGI Header + +This usually means: + +1. HTTP traffic is being sent to a uWSGI endpoint +2. The packet is malformed or truncated +3. Network issues caused data corruption + +Verify that nginx is using `uwsgi_pass` (not `proxy_pass`) and that the +`uwsgi_params` file is being included. + +### Headers Missing + +If certain headers aren't reaching your application, verify they're included +in `uwsgi_params`. Custom headers should be passed as: + +```nginx +uwsgi_param HTTP_X_CUSTOM_HEADER $http_x_custom_header; +``` + +## See Also + +- [Settings Reference](reference/settings.md#protocol) - Protocol and uWSGI settings +- [Deploy](deploy.md) - General deployment guidance +- [Design](design.md) - Worker architecture overview diff --git a/docs/macros.py b/docs/macros.py new file mode 100644 index 00000000..093476ee --- /dev/null +++ b/docs/macros.py @@ -0,0 +1,11 @@ +from importlib import import_module + +def define_env(env): + """Register template variables for MkDocs macros.""" + gunicorn = import_module("gunicorn") + env.variables.update( + release=gunicorn.__version__, + version=gunicorn.__version__, + github_repo="https://github.com/benoitc/gunicorn", + pypi_url=f"https://pypi.org/project/gunicorn/{gunicorn.__version__}/", + ) diff --git a/docs/modernization-plan.md b/docs/modernization-plan.md new file mode 100644 index 00000000..6c04bb4d --- /dev/null +++ b/docs/modernization-plan.md @@ -0,0 +1,35 @@ +# Website Modernization Plan + +## Goals +- Serve a single, canonical domain backed by a static MkDocs build. +- Keep the documentation authoring experience entirely in Markdown. +- Modernize the marketing home page with a refreshed visual identity. +- Preserve the generated settings reference sourced from Python code. + +## Architecture Overview +- **Static site generator:** MkDocs with the Material theme. +- **Content layout:** Markdown files in `docs/content/`, grouped by guides, reference, and news archives. +- **Styling:** Lightweight CSS overrides in `docs/content/styles/overrides.css` for hero, feature cards, and color palette. +- **Dynamic data:** `docs/macros.py` exposes the Gunicorn version, while `scripts/build_settings_doc.py` renders the settings reference into Markdown during every build. +- **Assets:** SVG mascot and hero art live under `docs/content/assets/` so both the homepage and docs share the same branding. + +## Completed Work +- Removed Sphinx configuration, themes, and the legacy static snapshot under `docs/site/`. +- Converted the entire content library (guides, FAQ, design notes, yearly news) from MyST/RST to MkDocs-friendly Markdown. +- Rebuilt the homepage using Material’s layout primitives with responsive hero, CTAs, and feature cards. +- Added CSS overrides that mirror Gunicorn’s brand colors and support light/dark modes. +- Replaced the Sphinx extension with a standalone Markdown generator for the settings reference. +- Introduced an automated MkDocs workflow (`.github/workflows/docs.yml`) that builds on every push and deploys to `gh-pages` from the `main` branch. + +## Remaining Enhancements +1. **Visual polish:** produce updated screenshots/asciicasts for quickstart and deployment examples; add Open Graph imagery. +2. **Content review:** prune outdated news entries, tighten FAQs, and add framework-specific quickstarts (FastAPI, Flask, Django). +3. **Accessibility & internationalization:** run axe audits, ensure color contrast, and consider adding minimal localization support. +4. **Performance extras:** enable MkDocs search index minification and gzip the GitHub Pages output (served automatically once deployed). +5. **Contributor docs:** extend `CONTRIBUTING.md` with MkDocs authoring tips, link to preview artifacts, and describe the `mkdocs serve` workflow. + +## Deployment Checklist +- [x] Update DNS to point away from ReadTheDocs once `gh-pages` is published. +- [x] Verify `site_url` in `mkdocs.yml` for canonical URLs and sitemap generation. +- [x] Ensure `CNAME` (if required) is checked into `gh-pages` during deployment. +- [ ] Announce the migration to end-users and update links in READMEs and PyPI metadata. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..abe6ea3e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,131 @@ +site_name: Gunicorn +site_url: https://gunicorn.org +repo_url: https://github.com/benoitc/gunicorn +repo_name: benoitc/gunicorn +docs_dir: docs/content +use_directory_urls: true + +nav: + - Home: index.md + - Getting Started: + - Quickstart: quickstart.md + - Install: install.md + - Run: run.md + - Configure: configure.md + - Guides: + - Deploy: deploy.md + - Docker: guides/docker.md + - ASGI Worker: asgi.md + - uWSGI Protocol: uwsgi.md + - Signals: signals.md + - Instrumentation: instrumentation.md + - Custom: custom.md + - Design: design.md + - Community: + - Overview: community.md + - FAQ: faq.md + - Reference: + - Settings: reference/settings.md + - News: + - Latest: news.md + - '2026': 2026-news.md + - '2024': 2024-news.md + - '2023': 2023-news.md + - '2021': 2021-news.md + - '2020': 2020-news.md + - '2019': 2019-news.md + - '2018': 2018-news.md + - '2017': 2017-news.md + - '2016': 2016-news.md + - '2015': 2015-news.md + - '2014': 2014-news.md + - '2013': 2013-news.md + - '2012': 2012-news.md + - '2011': 2011-news.md + - '2010': 2010-news.md + +theme: + name: material + custom_dir: overrides + language: en + logo: assets/gunicorn.svg + favicon: assets/gunicorn.svg + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: green + accent: teal + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: green + accent: teal + toggle: + icon: material/brightness-4 + name: Switch to light mode + font: + text: Inter + code: JetBrains Mono + features: + - content.code.copy + - content.code.annotate + - navigation.instant + - navigation.instant.progress + - navigation.tracking + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - navigation.path + - search.highlight + - search.suggest + - search.share + - toc.follow + - toc.integrate + - header.autohide + +plugins: + - search + - macros + - gen-files: + scripts: + - scripts/build_settings_doc.py + +markdown_extensions: + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.details + - pymdownx.highlight + - pymdownx.inlinehilite + - pymdownx.magiclink + - pymdownx.superfences + - pymdownx.snippets: + base_path: + - . + check_paths: true + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + +extra_css: + - styles/overrides.css + - assets/stylesheets/home.css + +extra_javascript: + - assets/javascripts/toc-collapse.js + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/benoitc/gunicorn + - icon: fontawesome/brands/python + link: https://pypi.org/project/gunicorn/ diff --git a/overrides/home.html b/overrides/home.html new file mode 100644 index 00000000..8defc14c --- /dev/null +++ b/overrides/home.html @@ -0,0 +1,30 @@ +{% extends "main.html" %} + +{% block tabs %} +{{ super() }} +{% endblock %} + +{% block htmltitle %} +Gunicorn - Python WSGI HTTP Server for UNIX +{% endblock %} + +{% block styles %} +{{ super() }} + +{% endblock %} + +{% block hero %}{% endblock %} + +{% block content %}{% endblock %} + +{% block site_nav %}{% endblock %} + +{% block container %} +
+ {{ page.content }} +
+{% endblock %} + +{% block footer %} +{{ super() }} +{% endblock %} diff --git a/requirements_dev.txt b/requirements_dev.txt index 1d8c0129..40b6dae6 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -5,5 +5,8 @@ # otherwise, oldest known-working version is 61.2 setuptools>=68.0 -sphinx -sphinx_rtd_theme +mkdocs>=1.6 +mkdocs-material>=9.5 +mkdocs-gen-files>=0.5 +mkdocs-macros-plugin>=1.0 +pymdown-extensions>=10.0 diff --git a/scripts/build_settings_doc.py b/scripts/build_settings_doc.py new file mode 100644 index 00000000..75174def --- /dev/null +++ b/scripts/build_settings_doc.py @@ -0,0 +1,254 @@ +"""Generate the Markdown settings reference for MkDocs.""" +from __future__ import annotations + +import inspect +import textwrap +from pathlib import Path +from typing import List + +import re + +import gunicorn.config as guncfg + +HEAD = """\ +> **Generated file** — update `gunicorn/config.py` instead. + +# Settings + +This reference is built directly from `gunicorn.config.KNOWN_SETTINGS` and is +regenerated during every documentation build. + +!!! note + Settings can be provided through the `GUNICORN_CMD_ARGS` environment + variable. For example: + + ```console + $ GUNICORN_CMD_ARGS="--bind=127.0.0.1 --workers=3" gunicorn app:app + ``` + + _Added in 19.7._ + +""" + + +def _format_default(setting: guncfg.Setting) -> tuple[str, bool]: + if hasattr(setting, "default_doc"): + text = textwrap.dedent(setting.default_doc).strip("\n") + return text, True + default = setting.default + if callable(default): + source = textwrap.dedent(inspect.getsource(default)).strip("\n") + return f"```python\n{source}\n```", True + if default == "": + return "`''`", False + return f"`{default!r}`", False + + +def _format_cli(setting: guncfg.Setting) -> str | None: + if not setting.cli: + return None + if setting.meta: + variants = [f"`{opt} {setting.meta}`" for opt in setting.cli] + else: + variants = [f"`{opt}`" for opt in setting.cli] + return ", ".join(variants) + + +REF_MAP = { + "forwarded-allow-ips": ("reference/settings.md", "forwarded_allow_ips"), + "forwarder-headers": ("reference/settings.md", "forwarder_headers"), + "proxy-allow-ips": ("reference/settings.md", "proxy_allow_ips"), + "worker-class": ("reference/settings.md", "worker_class"), + "reload": ("reference/settings.md", "reload"), + "raw-env": ("reference/settings.md", "raw_env"), + "check-config": ("reference/settings.md", "check_config"), + "errorlog": ("reference/settings.md", "errorlog"), + "logconfig": ("reference/settings.md", "logconfig"), + "logconfig-json": ("reference/settings.md", "logconfig_json"), + "ssl-context": ("reference/settings.md", "ssl_context"), + "ssl-version": ("reference/settings.md", "ssl_version"), + "blocking-os-fchmod": ("reference/settings.md", "blocking_os_fchmod"), + "configuration_file": ("../configure.md", "configuration-file"), +} + +REF_PATTERN = re.compile(r":ref:`([^`]+)`") + + +def _convert_refs(text: str) -> str: + def repl(match: re.Match[str]) -> str: + raw = match.group(1) + if "<" in raw and raw.endswith(">"): + label, target = raw.split("<", 1) + target = target[:-1] + label = label.replace("\n", " ").strip() + else: + label, target = None, raw.strip() + info = REF_MAP.get(target) + if not info: + return (label or target).replace("\n", " ").strip() + path, anchor = info + if path.endswith(".md"): + if path == "reference/settings.md" and anchor: + href = f"#{anchor}" + else: + href = path + (f"#{anchor}" if anchor else "") + else: + href = path + (f"#{anchor}" if anchor else "") + text = (label or target).replace("\n", " ").strip() + return f"[{text}]({href})" + + return REF_PATTERN.sub(repl, text) + + +def _consume_indented(lines: List[str], start: int) -> tuple[str, int]: + body: List[str] = [] + i = start + while i < len(lines): + line = lines[i] + if line.startswith(" ") or not line.strip(): + body.append(line) + i += 1 + else: + break + text = textwrap.dedent("\n".join(body)).strip("\n") + return text, i + + +def _convert_desc(desc: str) -> str: + raw_lines = textwrap.dedent(desc).splitlines() + output: List[str] = [] + i = 0 + while i < len(raw_lines): + line = raw_lines[i] + stripped = line.strip() + if stripped.startswith(".. note::"): + body, i = _consume_indented(raw_lines, i + 1) + output.append("!!! note") + if body: + for body_line in body.splitlines(): + output.append(f" {body_line}" if body_line else "") + output.append("") + continue + if stripped.startswith(".. warning::"): + body, i = _consume_indented(raw_lines, i + 1) + output.append("!!! warning") + if body: + for body_line in body.splitlines(): + output.append(f" {body_line}" if body_line else "") + output.append("") + continue + if stripped.startswith(".. deprecated::"): + version = stripped.split("::", 1)[1].strip() + body, i = _consume_indented(raw_lines, i + 1) + title = f"Deprecated in {version}" if version else "Deprecated" + output.append(f"!!! danger \"{title}\"") + if body: + for body_line in body.splitlines(): + output.append(f" {body_line}" if body_line else "") + output.append("") + continue + if stripped.startswith(".. versionadded::"): + version = stripped.split("::", 1)[1].strip() + body, i = _consume_indented(raw_lines, i + 1) + title = f"Added in {version}" if version else "Added" + output.append(f"!!! info \"{title}\"") + if body: + for body_line in body.splitlines(): + output.append(f" {body_line}" if body_line else "") + output.append("") + continue + if stripped.startswith(".. versionchanged::"): + version = stripped.split("::", 1)[1].strip() + body, i = _consume_indented(raw_lines, i + 1) + title = f"Changed in {version}" if version else "Changed" + output.append(f"!!! info \"{title}\"") + if body: + for body_line in body.splitlines(): + output.append(f" {body_line}" if body_line else "") + output.append("") + continue + if stripped.startswith(".. code::") or stripped.startswith(".. code-block::"): + language = stripped.split("::", 1)[1].strip() + body, i = _consume_indented(raw_lines, i + 1) + fence = language or "text" + output.append(f"```{fence}") + if body: + output.append(body) + output.append("```") + output.append("") + continue + + output.append(line) + i += 1 + + text = "\n".join(output) + text = _convert_refs(text) + # Collapse excessive blank lines + text = re.sub(r"\n{3,}", "\n\n", text) + return text.strip("\n") + + +def _format_setting(setting: guncfg.Setting) -> str: + lines: list[str] = [f"### `{setting.name}`", ""] + + cli = _format_cli(setting) + if cli: + lines.extend((f"**Command line:** {cli}", "")) + + default_text, is_block = _format_default(setting) + if is_block: + lines.append("**Default:**") + lines.append("") + lines.append(default_text) + else: + lines.append(f"**Default:** {default_text}") + lines.append("") + + desc = _convert_desc(setting.desc) + if desc: + lines.append(desc) + lines.append("") + + return "\n".join(lines) + + +def render_settings() -> str: + sections: list[str] = [HEAD, '', ""] + known_settings = sorted(guncfg.KNOWN_SETTINGS, key=lambda s: s.section) + current_section: str | None = None + + for setting in known_settings: + if setting.section != current_section: + current_section = setting.section + sections.append(f"## {current_section}\n") + sections.append(_format_setting(setting)) + + return "\n".join(sections).strip() + "\n" + + +def _write_output(markdown: str) -> None: + try: + import mkdocs_gen_files # type: ignore + except ImportError: + mkdocs_gen_files = None + + if mkdocs_gen_files is not None: + try: + with mkdocs_gen_files.open("reference/settings.md", "w") as fh: + fh.write(markdown) + return + except Exception: + pass + + output = Path(__file__).resolve().parents[1] / "docs" / "content" / "reference" / "settings.md" + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(markdown, encoding="utf-8") + + +def main() -> None: + markdown = render_settings() + _write_output(markdown) + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..d933d086 --- /dev/null +++ b/uv.lock @@ -0,0 +1,643 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[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/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/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/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/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/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/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 = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "eventlet" +version = "0.40.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "greenlet" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/d8/f72d8583db7c559445e0e9500a9b9787332370c16980802204a403634585/eventlet-0.40.4.tar.gz", hash = "sha256:69bef712b1be18b4930df6f0c495d2a882bf7b63aa111e7b6eeff461cfcaf26f", size = 565920, upload-time = "2025-11-26T13:57:31.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/6d/8e1fa901f6a8307f90e7bd932064e27a0062a4a7a16af38966a9c3293c52/eventlet-0.40.4-py3-none-any.whl", hash = "sha256:6326c6d0bf55810bece151f7a5750207c610f389ba110ffd1541ed6e5215485b", size = 364588, upload-time = "2025-11-26T13:57:29.09Z" }, +] + +[[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 = "gevent" +version = "25.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, + { name = "greenlet", marker = "platform_python_implementation == 'CPython'" }, + { name = "zope-event" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/48/b3ef2673ffb940f980966694e40d6d32560f3ffa284ecaeb5ea3a90a6d3f/gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd", size = 5059025, upload-time = "2025-09-17T16:15:34.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/c7/2c60fc4e5c9144f2b91e23af8d87c626870ad3183cfd09d2b3ba6d699178/gevent-25.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:856b990be5590e44c3a3dc6c8d48a40eaccbb42e99d2b791d11d1e7711a4297e", size = 1831980, upload-time = "2025-09-17T15:41:22.597Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ae/49bf0a01f95a1c92c001d7b3f482a2301626b8a0617f448c4cd14ca9b5d4/gevent-25.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fe1599d0b30e6093eb3213551751b24feeb43db79f07e89d98dd2f3330c9063e", size = 1918777, upload-time = "2025-09-17T15:48:57.223Z" }, + { url = "https://files.pythonhosted.org/packages/88/3f/266d2eb9f5d75c184a55a39e886b53a4ea7f42ff31f195220a363f0e3f9e/gevent-25.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f0d8b64057b4bf1529b9ef9bd2259495747fba93d1f836c77bfeaacfec373fd0", size = 1869235, upload-time = "2025-09-17T15:49:18.255Z" }, + { url = "https://files.pythonhosted.org/packages/76/24/c0c7c7db70ca74c7b1918388ebda7c8c2a3c3bff0bbfbaa9280ed04b3340/gevent-25.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b56cbc820e3136ba52cd690bdf77e47a4c239964d5f80dc657c1068e0fe9521c", size = 2177334, upload-time = "2025-09-17T15:15:10.073Z" }, + { url = "https://files.pythonhosted.org/packages/4c/1e/de96bd033c03955f54c455b51a5127b1d540afcfc97838d1801fafce6d2e/gevent-25.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5fa9ce5122c085983e33e0dc058f81f5264cebe746de5c401654ab96dddfca8", size = 1847708, upload-time = "2025-09-17T15:52:38.475Z" }, + { url = "https://files.pythonhosted.org/packages/26/8b/6851e9cd3e4f322fa15c1d196cbf1a8a123da69788b078227dd13dd4208f/gevent-25.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:03c74fec58eda4b4edc043311fca8ba4f8744ad1632eb0a41d5ec25413581975", size = 2234274, upload-time = "2025-09-17T15:24:07.797Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d8/b1178b70538c91493bec283018b47c16eab4bac9ddf5a3d4b7dd905dab60/gevent-25.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8ae9f895e8651d10b0a8328a61c9c53da11ea51b666388aa99b0ce90f9fdc27", size = 1695326, upload-time = "2025-09-17T20:10:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/81/86/03f8db0704fed41b0fa830425845f1eb4e20c92efa3f18751ee17809e9c6/gevent-25.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5aff9e8342dc954adb9c9c524db56c2f3557999463445ba3d9cbe3dada7b7", size = 1792418, upload-time = "2025-09-17T15:41:24.384Z" }, + { url = "https://files.pythonhosted.org/packages/5f/35/f6b3a31f0849a62cfa2c64574bcc68a781d5499c3195e296e892a121a3cf/gevent-25.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1cdf6db28f050ee103441caa8b0448ace545364f775059d5e2de089da975c457", size = 1875700, upload-time = "2025-09-17T15:48:59.652Z" }, + { url = "https://files.pythonhosted.org/packages/66/1e/75055950aa9b48f553e061afa9e3728061b5ccecca358cef19166e4ab74a/gevent-25.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:812debe235a8295be3b2a63b136c2474241fa5c58af55e6a0f8cfc29d4936235", size = 1831365, upload-time = "2025-09-17T15:49:19.426Z" }, + { url = "https://files.pythonhosted.org/packages/31/e8/5c1f6968e5547e501cfa03dcb0239dff55e44c3660a37ec534e32a0c008f/gevent-25.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b28b61ff9216a3d73fe8f35669eefcafa957f143ac534faf77e8a19eb9e6883a", size = 2122087, upload-time = "2025-09-17T15:15:12.329Z" }, + { url = "https://files.pythonhosted.org/packages/c0/2c/ebc5d38a7542af9fb7657bfe10932a558bb98c8a94e4748e827d3823fced/gevent-25.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e4b6278b37373306fc6b1e5f0f1cf56339a1377f67c35972775143d8d7776ff", size = 1808776, upload-time = "2025-09-17T15:52:40.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/26/e1d7d6c8ffbf76fe1fbb4e77bdb7f47d419206adc391ec40a8ace6ebbbf0/gevent-25.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d99f0cb2ce43c2e8305bf75bee61a8bde06619d21b9d0316ea190fc7a0620a56", size = 2179141, upload-time = "2025-09-17T15:24:09.895Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6c/bb21fd9c095506aeeaa616579a356aa50935165cc0f1e250e1e0575620a7/gevent-25.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:72152517ecf548e2f838c61b4be76637d99279dbaa7e01b3924df040aa996586", size = 1677941, upload-time = "2025-09-17T19:59:50.185Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/e55930ba5259629eb28ac7ee1abbca971996a9165f902f0249b561602f24/gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86", size = 2955991, upload-time = "2025-09-17T14:52:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/aa/88/63dc9e903980e1da1e16541ec5c70f2b224ec0a8e34088cb42794f1c7f52/gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692", size = 1808503, upload-time = "2025-09-17T15:41:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/7a/8d/7236c3a8f6ef7e94c22e658397009596fa90f24c7d19da11ad7ab3a9248e/gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2", size = 1890001, upload-time = "2025-09-17T15:49:01.227Z" }, + { url = "https://files.pythonhosted.org/packages/4f/63/0d7f38c4a2085ecce26b50492fc6161aa67250d381e26d6a7322c309b00f/gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74", size = 1855335, upload-time = "2025-09-17T15:49:20.582Z" }, + { url = "https://files.pythonhosted.org/packages/95/18/da5211dfc54c7a57e7432fd9a6ffeae1ce36fe5a313fa782b1c96529ea3d/gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51", size = 2109046, upload-time = "2025-09-17T15:15:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5a/7bb5ec8e43a2c6444853c4a9f955f3e72f479d7c24ea86c95fb264a2de65/gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5", size = 1827099, upload-time = "2025-09-17T15:52:41.384Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d4/b63a0a60635470d7d986ef19897e893c15326dd69e8fb342c76a4f07fe9e/gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f", size = 2172623, upload-time = "2025-09-17T15:24:12.03Z" }, + { url = "https://files.pythonhosted.org/packages/d5/98/caf06d5d22a7c129c1fb2fc1477306902a2c8ddfd399cd26bbbd4caf2141/gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3", size = 1682837, upload-time = "2025-09-17T19:48:47.318Z" }, + { url = "https://files.pythonhosted.org/packages/5a/77/b97f086388f87f8ad3e01364f845004aef0123d4430241c7c9b1f9bde742/gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed", size = 2973739, upload-time = "2025-09-17T14:53:30.279Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/9d5f204ead343e5b27bbb2fedaec7cd0009d50696b2266f590ae845d0331/gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245", size = 1809165, upload-time = "2025-09-17T15:41:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/10/3e/791d1bf1eb47748606d5f2c2aa66571f474d63e0176228b1f1fd7b77ab37/gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82", size = 1890638, upload-time = "2025-09-17T15:49:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5c/9ad0229b2b4d81249ca41e4f91dd8057deaa0da6d4fbe40bf13cdc5f7a47/gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48", size = 1857118, upload-time = "2025-09-17T15:49:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/49/2a/3010ed6c44179a3a5c5c152e6de43a30ff8bc2c8de3115ad8733533a018f/gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7", size = 2111598, upload-time = "2025-09-17T15:15:15.226Z" }, + { url = "https://files.pythonhosted.org/packages/08/75/6bbe57c19a7aa4527cc0f9afcdf5a5f2aed2603b08aadbccb5bf7f607ff4/gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47", size = 1829059, upload-time = "2025-09-17T15:52:42.596Z" }, + { url = "https://files.pythonhosted.org/packages/06/6e/19a9bee9092be45679cb69e4dd2e0bf5f897b7140b4b39c57cc123d24829/gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117", size = 2173529, upload-time = "2025-09-17T15:24:13.897Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4f/50de9afd879440e25737e63f5ba6ee764b75a3abe17376496ab57f432546/gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa", size = 1681518, upload-time = "2025-09-17T19:39:47.488Z" }, + { url = "https://files.pythonhosted.org/packages/15/1a/948f8167b2cdce573cf01cec07afc64d0456dc134b07900b26ac7018b37e/gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1a3fe4ea1c312dbf6b375b416925036fe79a40054e6bf6248ee46526ea628be1", size = 2982934, upload-time = "2025-09-17T14:54:11.302Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ec/726b146d1d3aad82e03d2e1e1507048ab6072f906e83f97f40667866e582/gevent-25.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0adb937f13e5fb90cca2edf66d8d7e99d62a299687400ce2edee3f3504009356", size = 1813982, upload-time = "2025-09-17T15:41:28.506Z" }, + { url = "https://files.pythonhosted.org/packages/35/5d/5f83f17162301662bd1ce702f8a736a8a8cac7b7a35e1d8b9866938d1f9d/gevent-25.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:427f869a2050a4202d93cf7fd6ab5cffb06d3e9113c10c967b6e2a0d45237cb8", size = 1894902, upload-time = "2025-09-17T15:49:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/83/cd/cf5e74e353f60dab357829069ffc300a7bb414c761f52cf8c0c6e9728b8d/gevent-25.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c049880175e8c93124188f9d926af0a62826a3b81aa6d3074928345f8238279e", size = 1861792, upload-time = "2025-09-17T15:49:23.279Z" }, + { url = "https://files.pythonhosted.org/packages/dd/65/b9a4526d4a4edce26fe4b3b993914ec9dc64baabad625a3101e51adb17f3/gevent-25.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5a67a0974ad9f24721034d1e008856111e0535f1541499f72a733a73d658d1c", size = 2113215, upload-time = "2025-09-17T15:15:16.34Z" }, + { url = "https://files.pythonhosted.org/packages/e5/be/7d35731dfaf8370795b606e515d964a0967e129db76ea7873f552045dd39/gevent-25.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d0f5d8d73f97e24ea8d24d8be0f51e0cf7c54b8021c1fddb580bf239474690f", size = 1833449, upload-time = "2025-09-17T15:52:43.75Z" }, + { url = "https://files.pythonhosted.org/packages/65/58/7bc52544ea5e63af88c4a26c90776feb42551b7555a1c89c20069c168a3f/gevent-25.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddd3ff26e5c4240d3fbf5516c2d9d5f2a998ef87cfb73e1429cfaeaaec860fa6", size = 2176034, upload-time = "2025-09-17T15:24:15.676Z" }, + { url = "https://files.pythonhosted.org/packages/c2/69/a7c4ba2ffbc7c7dbf6d8b4f5d0f0a421f7815d229f4909854266c445a3d4/gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7", size = 1703019, upload-time = "2025-09-17T19:30:55.272Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/0ae86792fb212e4384041e0ef8e7bc66f59a54912ce407d26a966ed2914d/greenlet-3.3.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b", size = 597403, upload-time = "2025-12-04T15:07:10.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/d4e73f5dfa888364bbf02efa85616c6714ae7c631c201349782e5b428925/greenlet-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082", size = 300740, upload-time = "2025-12-04T14:47:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, + { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, +] + +[[package]] +name = "gunicorn" +source = { editable = "." } +dependencies = [ + { name = "packaging" }, +] + +[package.optional-dependencies] +eventlet = [ + { name = "eventlet" }, +] +gevent = [ + { name = "gevent" }, +] +setproctitle = [ + { name = "setproctitle" }, +] +testing = [ + { name = "coverage" }, + { name = "eventlet" }, + { name = "gevent" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] +tornado = [ + { name = "tornado" }, +] + +[package.metadata] +requires-dist = [ + { name = "coverage", marker = "extra == 'testing'" }, + { name = "eventlet", marker = "extra == 'eventlet'", specifier = ">=0.40.3" }, + { name = "eventlet", marker = "extra == 'testing'", specifier = ">=0.40.3" }, + { name = "gevent", marker = "extra == 'gevent'", specifier = ">=23.9.0" }, + { name = "gevent", marker = "extra == 'testing'", specifier = ">=23.9.0" }, + { name = "packaging" }, + { name = "pytest", marker = "extra == 'testing'" }, + { name = "pytest-asyncio", marker = "extra == 'testing'" }, + { name = "pytest-cov", marker = "extra == 'testing'" }, + { name = "setproctitle", marker = "extra == 'setproctitle'" }, + { name = "tornado", marker = "extra == 'tornado'", specifier = ">=6.5.0" }, +] +provides-extras = ["gevent", "eventlet", "tornado", "gthread", "setproctitle", "testing"] + +[[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 = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[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.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +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/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "setproctitle" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/48/fb401ec8c4953d519d05c87feca816ad668b8258448ff60579ac7a1c1386/setproctitle-1.3.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf555b6299f10a6eb44e4f96d2f5a3884c70ce25dc5c8796aaa2f7b40e72cb1b", size = 18079, upload-time = "2025-09-05T12:49:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/c2b0333c2716fb3b4c9a973dd113366ac51b4f8d56b500f4f8f704b4817a/setproctitle-1.3.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:690b4776f9c15aaf1023bb07d7c5b797681a17af98a4a69e76a1d504e41108b7", size = 13099, upload-time = "2025-09-05T12:49:09.222Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f8/17bda581c517678260e6541b600eeb67745f53596dc077174141ba2f6702/setproctitle-1.3.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00afa6fc507967d8c9d592a887cdc6c1f5742ceac6a4354d111ca0214847732c", size = 31793, upload-time = "2025-09-05T12:49:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/27/d1/76a33ae80d4e788ecab9eb9b53db03e81cfc95367ec7e3fbf4989962fedd/setproctitle-1.3.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e02667f6b9fc1238ba753c0f4b0a37ae184ce8f3bbbc38e115d99646b3f4cd3", size = 32779, upload-time = "2025-09-05T12:49:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/59/27/1a07c38121967061564f5e0884414a5ab11a783260450172d4fc68c15621/setproctitle-1.3.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83fcd271567d133eb9532d3b067c8a75be175b2b3b271e2812921a05303a693f", size = 34578, upload-time = "2025-09-05T12:49:13.393Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d4/725e6353935962d8bb12cbf7e7abba1d0d738c7f6935f90239d8e1ccf913/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13fe37951dda1a45c35d77d06e3da5d90e4f875c4918a7312b3b4556cfa7ff64", size = 32030, upload-time = "2025-09-05T12:49:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/e4677ae8e1cb0d549ab558b12db10c175a889be0974c589c428fece5433e/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a05509cfb2059e5d2ddff701d38e474169e9ce2a298cf1b6fd5f3a213a553fe5", size = 33363, upload-time = "2025-09-05T12:49:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/55/d4/69ce66e4373a48fdbb37489f3ded476bb393e27f514968c3a69a67343ae0/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6da835e76ae18574859224a75db6e15c4c2aaa66d300a57efeaa4c97ca4c7381", size = 31508, upload-time = "2025-09-05T12:49:18.032Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5a/42c1ed0e9665d068146a68326529b5686a1881c8b9197c2664db4baf6aeb/setproctitle-1.3.7-cp310-cp310-win32.whl", hash = "sha256:9e803d1b1e20240a93bac0bc1025363f7f80cb7eab67dfe21efc0686cc59ad7c", size = 12558, upload-time = "2025-09-05T12:49:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fe/dd206cc19a25561921456f6cb12b405635319299b6f366e0bebe872abc18/setproctitle-1.3.7-cp310-cp310-win_amd64.whl", hash = "sha256:a97200acc6b64ec4cada52c2ecaf1fba1ef9429ce9c542f8a7db5bcaa9dcbd95", size = 13245, upload-time = "2025-09-05T12:49:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/04/cd/1b7ba5cad635510720ce19d7122154df96a2387d2a74217be552887c93e5/setproctitle-1.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a600eeb4145fb0ee6c287cb82a2884bd4ec5bbb076921e287039dcc7b7cc6dd0", size = 18085, upload-time = "2025-09-05T12:49:22.183Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/b2da0a620490aae355f9d72072ac13e901a9fec809a6a24fc6493a8f3c35/setproctitle-1.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97a090fed480471bb175689859532709e28c085087e344bca45cf318034f70c4", size = 13097, upload-time = "2025-09-05T12:49:23.322Z" }, + { url = "https://files.pythonhosted.org/packages/18/2e/bd03ff02432a181c1787f6fc2a678f53b7dacdd5ded69c318fe1619556e8/setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f", size = 32191, upload-time = "2025-09-05T12:49:24.567Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/1e62fc0937a8549f2220445ed2175daacee9b6764c7963b16148119b016d/setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9", size = 33203, upload-time = "2025-09-05T12:49:25.871Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/65edc65db3fa3df400cf13b05e9d41a3c77517b4839ce873aa6b4043184f/setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba", size = 34963, upload-time = "2025-09-05T12:49:27.044Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/89157e3de997973e306e44152522385f428e16f92f3cf113461489e1e2ee/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307", size = 32398, upload-time = "2025-09-05T12:49:28.909Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/77a765a339ddf046844cb4513353d8e9dcd8183da9cdba6e078713e6b0b2/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee", size = 33657, upload-time = "2025-09-05T12:49:30.323Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/f0b6205c64d74d2a24a58644a38ec77bdbaa6afc13747e75973bf8904932/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1", size = 31836, upload-time = "2025-09-05T12:49:32.309Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e1277f9ba302f1a250bbd3eedbbee747a244b3cc682eb58fb9733968f6d8/setproctitle-1.3.7-cp311-cp311-win32.whl", hash = "sha256:b74774ca471c86c09b9d5037c8451fff06bb82cd320d26ae5a01c758088c0d5d", size = 12556, upload-time = "2025-09-05T12:49:33.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/822a23f17e9003dfdee92cd72758441ca2a3680388da813a371b716fb07f/setproctitle-1.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:acb9097213a8dd3410ed9f0dc147840e45ca9797785272928d4be3f0e69e3be4", size = 13243, upload-time = "2025-09-05T12:49:34.553Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" }, + { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2f/fcedcade3b307a391b6e17c774c6261a7166aed641aee00ed2aad96c63ce/setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922", size = 18047, upload-time = "2025-09-05T12:49:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/23/ae/afc141ca9631350d0a80b8f287aac79a76f26b6af28fd8bf92dae70dc2c5/setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee", size = 13073, upload-time = "2025-09-05T12:49:51.46Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/0a4f00315bc02510395b95eec3d4aa77c07192ee79f0baae77ea7b9603d8/setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd", size = 33284, upload-time = "2025-09-05T12:49:52.741Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/adf3c4c0a2173cb7920dc9df710bcc67e9bcdbf377e243b7a962dc31a51a/setproctitle-1.3.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5d1cb3f81531f0eb40e13246b679a1bdb58762b170303463cb06ecc296f26d0", size = 34104, upload-time = "2025-09-05T12:49:54.416Z" }, + { url = "https://files.pythonhosted.org/packages/52/4f/6daf66394152756664257180439d37047aa9a1cfaa5e4f5ed35e93d1dc06/setproctitle-1.3.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a7d159e7345f343b44330cbba9194169b8590cb13dae940da47aa36a72aa9929", size = 35982, upload-time = "2025-09-05T12:49:56.295Z" }, + { url = "https://files.pythonhosted.org/packages/1b/62/f2c0595403cf915db031f346b0e3b2c0096050e90e0be658a64f44f4278a/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b5074649797fd07c72ca1f6bff0406f4a42e1194faac03ecaab765ce605866f", size = 33150, upload-time = "2025-09-05T12:49:58.025Z" }, + { url = "https://files.pythonhosted.org/packages/a0/29/10dd41cde849fb2f9b626c846b7ea30c99c81a18a5037a45cc4ba33c19a7/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61e96febced3f61b766115381d97a21a6265a0f29188a791f6df7ed777aef698", size = 34463, upload-time = "2025-09-05T12:49:59.424Z" }, + { url = "https://files.pythonhosted.org/packages/71/3c/cedd8eccfaf15fb73a2c20525b68c9477518917c9437737fa0fda91e378f/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c", size = 32848, upload-time = "2025-09-05T12:50:01.107Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3e/0a0e27d1c9926fecccfd1f91796c244416c70bf6bca448d988638faea81d/setproctitle-1.3.7-cp313-cp313-win32.whl", hash = "sha256:7f47accafac7fe6535ba8ba9efd59df9d84a6214565108d0ebb1199119c9cbbd", size = 12544, upload-time = "2025-09-05T12:50:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/6bf4cb7acbbd5c846ede1c3f4d6b4ee52744d402e43546826da065ff2ab7/setproctitle-1.3.7-cp313-cp313-win_amd64.whl", hash = "sha256:fe5ca35aeec6dc50cabab9bf2d12fbc9067eede7ff4fe92b8f5b99d92e21263f", size = 13235, upload-time = "2025-09-05T12:50:16.89Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a4/d588d3497d4714750e3eaf269e9e8985449203d82b16b933c39bd3fc52a1/setproctitle-1.3.7-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:10e92915c4b3086b1586933a36faf4f92f903c5554f3c34102d18c7d3f5378e9", size = 18058, upload-time = "2025-09-05T12:50:02.501Z" }, + { url = "https://files.pythonhosted.org/packages/05/77/7637f7682322a7244e07c373881c7e982567e2cb1dd2f31bd31481e45500/setproctitle-1.3.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de879e9c2eab637f34b1a14c4da1e030c12658cdc69ee1b3e5be81b380163ce5", size = 13072, upload-time = "2025-09-05T12:50:03.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/09/f366eca0973cfbac1470068d1313fa3fe3de4a594683385204ec7f1c4101/setproctitle-1.3.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c18246d88e227a5b16248687514f95642505000442165f4b7db354d39d0e4c29", size = 34490, upload-time = "2025-09-05T12:50:04.948Z" }, + { url = "https://files.pythonhosted.org/packages/71/36/611fc2ed149fdea17c3677e1d0df30d8186eef9562acc248682b91312706/setproctitle-1.3.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7081f193dab22df2c36f9fc6d113f3793f83c27891af8fe30c64d89d9a37e152", size = 35267, upload-time = "2025-09-05T12:50:06.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/a4/64e77d0671446bd5a5554387b69e1efd915274686844bea733714c828813/setproctitle-1.3.7-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cc9b901ce129350637426a89cfd650066a4adc6899e47822e2478a74023ff7c", size = 37376, upload-time = "2025-09-05T12:50:07.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/ad9c664fe524fb4a4b2d3663661a5c63453ce851736171e454fa2cdec35c/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80e177eff2d1ec172188d0d7fd9694f8e43d3aab76a6f5f929bee7bf7894e98b", size = 33963, upload-time = "2025-09-05T12:50:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a36de7caf2d90c4c28678da1466b47495cbbad43badb4e982d8db8167ed4/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:23e520776c445478a67ee71b2a3c1ffdafbe1f9f677239e03d7e2cc635954e18", size = 35550, upload-time = "2025-09-05T12:50:10.791Z" }, + { url = "https://files.pythonhosted.org/packages/dd/68/17e8aea0ed5ebc17fbf03ed2562bfab277c280e3625850c38d92a7b5fcd9/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5fa1953126a3b9bd47049d58c51b9dac72e78ed120459bd3aceb1bacee72357c", size = 33727, upload-time = "2025-09-05T12:50:12.032Z" }, + { url = "https://files.pythonhosted.org/packages/b2/33/90a3bf43fe3a2242b4618aa799c672270250b5780667898f30663fd94993/setproctitle-1.3.7-cp313-cp313t-win32.whl", hash = "sha256:4a5e212bf438a4dbeece763f4962ad472c6008ff6702e230b4f16a037e2f6f29", size = 12549, upload-time = "2025-09-05T12:50:13.074Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/50d1f07f3032e1f23d814ad6462bc0a138f369967c72494286b8a5228e40/setproctitle-1.3.7-cp313-cp313t-win_amd64.whl", hash = "sha256:cf2727b733e90b4f874bac53e3092aa0413fe1ea6d4f153f01207e6ce65034d9", size = 13243, upload-time = "2025-09-05T12:50:14.146Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" }, + { url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" }, + { url = "https://files.pythonhosted.org/packages/34/8a/aff5506ce89bc3168cb492b18ba45573158d528184e8a9759a05a09088a9/setproctitle-1.3.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:eb440c5644a448e6203935ed60466ec8d0df7278cd22dc6cf782d07911bcbea6", size = 12654, upload-time = "2025-09-05T12:51:17.141Z" }, + { url = "https://files.pythonhosted.org/packages/41/89/5b6f2faedd6ced3d3c085a5efbd91380fb1f61f4c12bc42acad37932f4e9/setproctitle-1.3.7-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:502b902a0e4c69031b87870ff4986c290ebbb12d6038a70639f09c331b18efb2", size = 14284, upload-time = "2025-09-05T12:51:18.393Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c0/4312fed3ca393a29589603fd48f17937b4ed0638b923bac75a728382e730/setproctitle-1.3.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f6f268caeabb37ccd824d749e7ce0ec6337c4ed954adba33ec0d90cc46b0ab78", size = 13282, upload-time = "2025-09-05T12:51:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5b/5e1c117ac84e3cefcf8d7a7f6b2461795a87e20869da065a5c087149060b/setproctitle-1.3.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1cac6a4b0252b8811d60b6d8d0f157c0fdfed379ac89c25a914e6346cf355a1", size = 12587, upload-time = "2025-09-05T12:51:21.195Z" }, + { url = "https://files.pythonhosted.org/packages/73/02/b9eadc226195dcfa90eed37afe56b5dd6fa2f0e5220ab8b7867b8862b926/setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65", size = 14286, upload-time = "2025-09-05T12:51:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/1be1d2a53c2a91ec48fa2ff4a409b395f836798adf194d99de9c059419ea/setproctitle-1.3.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b08b61976ffa548bd5349ce54404bf6b2d51bd74d4f1b241ed1b0f25bce09c3a", size = 13282, upload-time = "2025-09-05T12:51:24.094Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, +] + +[[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" }, +] + +[[package]] +name = "zope-event" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/33/d3eeac228fc14de76615612ee208be2d8a5b5b0fada36bf9b62d6b40600c/zope_event-6.1.tar.gz", hash = "sha256:6052a3e0cb8565d3d4ef1a3a7809336ac519bc4fe38398cb8d466db09adef4f0", size = 18739, upload-time = "2025-11-07T08:05:49.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/b0/956902e5e1302f8c5d124e219c6bf214e2649f92ad5fce85b05c039a04c9/zope_event-6.1-py3-none-any.whl", hash = "sha256:0ca78b6391b694272b23ec1335c0294cc471065ed10f7f606858fc54566c25a0", size = 6414, upload-time = "2025-11-07T08:05:48.874Z" }, +] + +[[package]] +name = "zope-interface" +version = "8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/a4/77daa5ba398996d16bb43fc721599d27d03eae68fe3c799de1963c72e228/zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224", size = 254019, upload-time = "2026-01-09T07:51:07.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fa/6d9eb3a33998a3019d7eb4fa1802d01d6602fad90e0aea443e6e0fe8e49a/zope_interface-8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:788c293f3165964ec6527b2d861072c68eef53425213f36d3893ebee89a89623", size = 207541, upload-time = "2026-01-09T08:04:55.378Z" }, + { url = "https://files.pythonhosted.org/packages/19/8c/ad23c96fdee84cb1f768f6695dac187cc26e9038e01c69713ba0f7dc46ab/zope_interface-8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9a4e785097e741a1c953b3970ce28f2823bd63c00adc5d276f2981dd66c96c15", size = 208075, upload-time = "2026-01-09T08:04:57.118Z" }, + { url = "https://files.pythonhosted.org/packages/dd/35/1bfd5fec31a307f0cf4065ee74ade63858ded3e2a71e248f1508118fcc95/zope_interface-8.2-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:16c69da19a06566664ddd4785f37cad5693a51d48df1515d264c20d005d322e2", size = 249528, upload-time = "2026-01-09T08:04:59.074Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3a/5d50b5fdb0f8226a2edff6adb7efdd3762ec95dff827dbab1761cb9a9e85/zope_interface-8.2-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c31acfa3d7cde48bec45701b0e1f4698daffc378f559bfb296837d8c834732f6", size = 254646, upload-time = "2026-01-09T08:05:00.964Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2a/ee7d675e151578eaf77828b8faac2b7ed9a69fead350bf5cf0e4afe7c73d/zope_interface-8.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0723507127f8269b8f3f22663168f717e9c9742107d1b6c9f419df561b71aa6d", size = 255083, upload-time = "2026-01-09T08:05:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/5d/07/99e2342f976c3700e142eddc01524e375a9e9078869a6885d9c72f3a3659/zope_interface-8.2-cp310-cp310-win_amd64.whl", hash = "sha256:3bf73a910bb27344def2d301a03329c559a79b308e1e584686b74171d736be4e", size = 211924, upload-time = "2026-01-09T08:05:04.702Z" }, + { url = "https://files.pythonhosted.org/packages/98/97/9c2aa8caae79915ed64eb114e18816f178984c917aa9adf2a18345e4f2e5/zope_interface-8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c65ade7ea85516e428651048489f5e689e695c79188761de8c622594d1e13322", size = 208081, upload-time = "2026-01-09T08:05:06.623Z" }, + { url = "https://files.pythonhosted.org/packages/34/86/4e2fcb01a8f6780ac84923748e450af0805531f47c0956b83065c99ab543/zope_interface-8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1ef4b43659e1348f35f38e7d1a6bbc1682efde239761f335ffc7e31e798b65b", size = 208522, upload-time = "2026-01-09T08:05:07.986Z" }, + { url = "https://files.pythonhosted.org/packages/f6/eb/08e277da32ddcd4014922854096cf6dcb7081fad415892c2da1bedefbf02/zope_interface-8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dfc4f44e8de2ff4eba20af4f0a3ca42d3c43ab24a08e49ccd8558b7a4185b466", size = 255198, upload-time = "2026-01-09T08:05:09.532Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a1/b32484f3281a5dc83bc713ad61eca52c543735cdf204543172087a074a74/zope_interface-8.2-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8f094bfb49179ec5dc9981cb769af1275702bd64720ef94874d9e34da1390d4c", size = 259970, upload-time = "2026-01-09T08:05:11.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/bca0e8ae1e487d4093a8a7cfed2118aa2d4758c8cfd66e59d2af09d71f1c/zope_interface-8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d2bb8e7364e18f083bf6744ccf30433b2a5f236c39c95df8514e3c13007098ce", size = 261153, upload-time = "2026-01-09T08:05:13.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/e3ff2a708011e56b10b271b038d4cb650a8ad5b7d24352fe2edf6d6b187a/zope_interface-8.2-cp311-cp311-win_amd64.whl", hash = "sha256:6f4b4dfcfdfaa9177a600bb31cebf711fdb8c8e9ed84f14c61c420c6aa398489", size = 212330, upload-time = "2026-01-09T08:05:15.267Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a0/1e1fabbd2e9c53ef92b69df6d14f4adc94ec25583b1380336905dc37e9a0/zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:624b6787fc7c3e45fa401984f6add2c736b70a7506518c3b537ffaacc4b29d4c", size = 208785, upload-time = "2026-01-09T08:05:17.348Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2a/88d098a06975c722a192ef1fb7d623d1b57c6a6997cf01a7aabb45ab1970/zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc9ded9e97a0ed17731d479596ed1071e53b18e6fdb2fc33af1e43f5fd2d3aaa", size = 208976, upload-time = "2026-01-09T08:05:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e8/757398549fdfd2f8c89f32c82ae4d2f0537ae2a5d2f21f4a2f711f5a059f/zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:532367553e4420c80c0fc0cabcc2c74080d495573706f66723edee6eae53361d", size = 259411, upload-time = "2026-01-09T08:05:20.567Z" }, + { url = "https://files.pythonhosted.org/packages/91/af/502601f0395ce84dff622f63cab47488657a04d0065547df42bee3a680ff/zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2bf9cf275468bafa3c72688aad8cfcbe3d28ee792baf0b228a1b2d93bd1d541a", size = 264859, upload-time = "2026-01-09T08:05:22.234Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/d2f765b9b4814a368a7c1b0ac23b68823c6789a732112668072fe596945d/zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0009d2d3c02ea783045d7804da4fd016245e5c5de31a86cebba66dd6914d59a2", size = 264398, upload-time = "2026-01-09T08:05:23.853Z" }, + { url = "https://files.pythonhosted.org/packages/4a/81/2f171fbc4222066957e6b9220c4fb9146792540102c37e6d94e5d14aad97/zope_interface-8.2-cp312-cp312-win_amd64.whl", hash = "sha256:845d14e580220ae4544bd4d7eb800f0b6034fe5585fc2536806e0a26c2ee6640", size = 212444, upload-time = "2026-01-09T08:05:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/66/47/45188fb101fa060b20e6090e500682398ab415e516a0c228fbb22bc7def2/zope_interface-8.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:6068322004a0158c80dfd4708dfb103a899635408c67c3b10e9acec4dbacefec", size = 209170, upload-time = "2026-01-09T08:05:26.616Z" }, + { url = "https://files.pythonhosted.org/packages/09/03/f6b9336c03c2b48403c4eb73a1ec961d94dc2fb5354c583dfb5fa05fd41f/zope_interface-8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2499de92e8275d0dd68f84425b3e19e9268cd1fa8507997900fa4175f157733c", size = 209229, upload-time = "2026-01-09T08:05:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/07/b1/65fe1dca708569f302ade02e6cdca309eab6752bc9f80105514f5b708651/zope_interface-8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f777e68c76208503609c83ca021a6864902b646530a1a39abb9ed310d1100664", size = 259393, upload-time = "2026-01-09T08:05:29.897Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a5/97b49cfceb6ed53d3dcfb3f3ebf24d83b5553194f0337fbbb3a9fec6cf78/zope_interface-8.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b05a919fdb0ed6ea942e5a7800e09a8b6cdae6f98fee1bef1c9d1a3fc43aaa0", size = 264863, upload-time = "2026-01-09T08:05:31.501Z" }, + { url = "https://files.pythonhosted.org/packages/cb/02/0b7a77292810efe3a0586a505b077ebafd5114e10c6e6e659f0c8e387e1f/zope_interface-8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ccc62b5712dd7bd64cfba3ee63089fb11e840f5914b990033beeae3b2180b6cb", size = 264369, upload-time = "2026-01-09T08:05:32.941Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1d/0d1ff3846302ed1b5bbf659316d8084b30106770a5f346b7ff4e9f540f80/zope_interface-8.2-cp313-cp313-win_amd64.whl", hash = "sha256:34f877d1d3bb7565c494ed93828fa6417641ca26faf6e8f044e0d0d500807028", size = 212447, upload-time = "2026-01-09T08:05:35.064Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/3c89de3917751446728b8898b4d53318bc2f8f6bf8196e150a063c59905e/zope_interface-8.2-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:46c7e4e8cbc698398a67e56ca985d19cb92365b4aafbeb6a712e8c101090f4cb", size = 209223, upload-time = "2026-01-09T08:05:36.449Z" }, + { url = "https://files.pythonhosted.org/packages/00/7f/62d00ec53f0a6e5df0c984781e6f3999ed265129c4c3413df8128d1e0207/zope_interface-8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a87fc7517f825a97ff4a4ca4c8a950593c59e0f8e7bfe1b6f898a38d5ba9f9cf", size = 209366, upload-time = "2026-01-09T08:05:38.197Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/f241986315174be8e00aabecfc2153cf8029c1327cab8ed53a9d979d7e08/zope_interface-8.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:ccf52f7d44d669203c2096c1a0c2c15d52e36b2e7a9413df50f48392c7d4d080", size = 261037, upload-time = "2026-01-09T08:05:39.568Z" }, + { url = "https://files.pythonhosted.org/packages/02/cc/b321c51d6936ede296a1b8860cf173bee2928357fe1fff7f97234899173f/zope_interface-8.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aae807efc7bd26302eb2fea05cd6de7d59269ed6ae23a6de1ee47add6de99b8c", size = 264219, upload-time = "2026-01-09T08:05:41.624Z" }, + { url = "https://files.pythonhosted.org/packages/ab/fb/5f5e7b40a2f4efd873fe173624795ca47eaa22e29051270c981361b45209/zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c", size = 264390, upload-time = "2026-01-09T08:05:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/f9/82/3f2bc594370bc3abd58e5f9085d263bf682a222f059ed46275cde0570810/zope_interface-8.2-cp314-cp314-win_amd64.whl", hash = "sha256:561ce42390bee90bae51cf1c012902a8033b2aaefbd0deed81e877562a116d48", size = 212585, upload-time = "2026-01-09T08:05:44.419Z" }, +]