diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..65098c18 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,72 @@ +name: Docs + +on: + push: + branches: [ main ] + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'requirements_dev.txt' + - '.github/workflows/docs.yml' + pull_request: + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'requirements_dev.txt' + - '.github/workflows/docs.yml' + +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 -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/main' + 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 -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 + commit_message: "docs: deploy {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/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/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/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..eda92ebb --- /dev/null +++ b/docs/content/design.md @@ -0,0 +1,83 @@ + +# Design + +A brief look at Gunicorn's architecture. + +## Server model + +Gunicorn uses a pre-fork worker model: a master process manages worker +processes, while the workers handle requests and responses. The master never +touches individual client sockets. + +### Master + +The master process listens for signals (TTIN, TTOU, CHLD, etc.) and adjusts the +worker pool accordingly. `TTIN`/`TTOU` change the number of workers; `CHLD` +indicates a worker exited and must be restarted. + +### Sync workers + +The default `sync` worker handles one request at a time. Errors affect only the +current request. Because connections close after each response, persistent +connections are not supported even if you set `Keep-Alive` headers manually. + +### Async workers + +Async workers are powered by [greenlets](https://github.com/python-greenlet/greenlet) +through [Eventlet](http://eventlet.net/) or [Gevent](http://www.gevent.org/). +Most apps work without modification, though full compatibility may require +patches (for example installing [`psycogreen`](https://github.com/psycopg/psycogreen/) +when using [Psycopg](http://initd.org/psycopg/)). Some apps that depend on the +original blocking behaviour may not be compatible. + +### Gthread workers + +`gthread` is a threaded worker. The main loop accepts connections and places +them in a thread pool. Keep-alive connections return to the pool to await +further events; idle connections close after the keepalive timeout. + +### Tornado workers + +A Tornado worker class exists for Tornado-based applications. While it can +serve WSGI apps, this configuration is not recommended. + + +### AsyncIO workers + +Use third-party workers to pair Gunicorn with asyncio frameworks (see the +[aiohttp deployment guide](https://docs.aiohttp.org/en/stable/deployment.html#nginx-gunicorn) +or the [Flask aiohttp example](https://github.com/benoitc/gunicorn/blob/master/examples/frameworks/flaskapp_aiohttp_wsgi.py)). + +## Choosing a worker type + +Synchronous workers assume your app is CPU/network bound and avoids indefinite +operations. Any outbound HTTP calls or other blocking behaviour benefit from an +async worker. Because synchronous workers are vulnerable to slow clients, +Gunicorn requires a buffering proxy in front of the default configuration. Tools +like [Hey](https://github.com/rakyll/hey) can simulate slow responses to test +this scenario. + +Examples that need async workers: + +- Long blocking calls (outbound web services) +- Direct internet traffic (no buffering proxy) +- Streaming request/response bodies +- Long polling +- WebSockets / Comet + +## How many workers? + +Do **not** scale workers to match client count. Gunicorn usually needs only 4–12 +workers to handle heavy traffic. Start with `(2 * num_cores) + 1` and adjust +under load using `TTIN`/`TTOU`. + +Too many workers waste resources and can reduce throughput. + +## How many threads? + +Since Gunicorn 19 you can set `--threads` (with the `gthread` worker) to process +requests concurrently. Threads can extend request time beyond the worker +timeout while still notifying the master. The optimal mix of threads and worker +processes depends on the runtime (for example CPython vs. Jython). Threads share +memory, lowering footprint, and still allow reloads because application code is +loaded in worker processes. 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/index.md b/docs/content/index.md new file mode 100644 index 00000000..c9a7079a --- /dev/null +++ b/docs/content/index.md @@ -0,0 +1,68 @@ +# Gunicorn + +
+
+
+ +

Production-ready Python web services

+

Gunicorn is a dependable WSGI HTTP server for UNIX that keeps Python applications running fast and resilient in production. Built on a pre-fork worker model and trusted in countless deployments, it pairs clean configuration with flexible worker strategies so you can meet any traffic pattern.

+ +
+
+
$ pip install gunicorn
+$ gunicorn example:app --workers 3
+
Latest release: {{ release }}
+
+
+
+ +## Quickstart + +1. Install Gunicorn into your application environment. +2. Point Gunicorn at your WSGI app: `gunicorn myproject.wsgi`. +3. Tune worker type, concurrency, and hooks using the rich [settings](reference/settings.md). + +Need a longer walkthrough? Jump into the [install guide](install.md). + +## Why teams choose Gunicorn + +
+
+

Works with your framework

+

Django, Flask, FastAPI, Pyramid, you name it—Gunicorn speaks WSGI so your stack just runs.

+ Running Gunicorn → +
+
+

Flexible workers

+

Sync, async, gevent, eventlet—choose the concurrency model that fits.

+ Worker classes → +
+
+

Battle-tested hooks

+

Lifecycle hooks let you instrument, reload, and extend Gunicorn to match your deployment requirements.

+ Server hooks → +
+
+

Containers to bare metal

+

Deploy with systemd, Kubernetes, Heroku, or Docker—the configuration stays predictable everywhere.

+ Deployment patterns → +
+
+ +## Documentation map + +- [Install](install.md): Set up Gunicorn in a clean environment. +- [Run](run.md): CLI usage and integration with frameworks. +- [Configure](configure.md): Combine CLI flags and config files effectively. +- [Settings reference](reference/settings.md): Generated from the Gunicorn source of truth. +- [Signals](signals.md): Manage worker lifecycle in production. +- [Instrumentation](instrumentation.md): Monitor metrics and logs. + +## Community & support + +- Report bugs or request features on [GitHub Issues](https://github.com/benoitc/gunicorn/issues). +- Discuss strategies with maintainers in `#gunicorn` on [Libera Chat](https://libera.chat/). +- Contributions are welcome—see the [contributing guide](community.md#contributing) and say hi to the maintainers. diff --git a/docs/content/install.md b/docs/content/install.md new file mode 100644 index 00000000..df69e64f --- /dev/null +++ b/docs/content/install.md @@ -0,0 +1,142 @@ +# Installation + +!!! note + Gunicorn requires **Python 3.12 or newer**. + + + +```bash +pip install gunicorn +``` + +## From source + +Install Gunicorn from GitHub if you want the latest development version: + +```bash +pip install git+https://github.com/benoitc/gunicorn.git +``` + +Stay current by upgrading in place: + +```bash +pip install -U git+https://github.com/benoitc/gunicorn.git +``` + +## Async workers + +Install Eventlet or Gevent if your application benefits from cooperative I/O. +Both rely on `greenlet`, so make sure the Python headers are available (for +example, install the `python-dev` package on Ubuntu). + +```bash +pip install greenlet # Required for both +pip install eventlet # For eventlet workers +pip install gunicorn[eventlet] # Or, using extra +pip install gevent # For gevent workers +pip install gunicorn[gevent] # Or, using extra +``` + +!!! note + Gevent also needs `libevent` 1.4.x or 2.0.4+. Install it from your package + manager or build it manually if the packaged version is too old. + + + +## Extra packages + +Some Gunicorn options require additional dependencies. Install them via +extras to pull everything in with one command. + +Most extras enable alternative worker types—see the +[design docs](design.md) for when each worker makes sense. + +- `gunicorn[eventlet]` — Eventlet-based greenlet workers +- `gunicorn[gevent]` — Gevent-based greenlet workers +- `gunicorn[gthread]` — Threaded workers +- `gunicorn[tornado]` — Tornado-based workers (not recommended) + +If you run more than one Gunicorn instance, the +[`proc_name`](reference/settings.md#proc_name) setting helps distinguish them in tools such +as `ps` and `top`. + +- `gunicorn[setproctitle]` — Enables setting the process name + +You can combine multiple extras, for example: + +```bash +pip install gunicorn[gevent,setproctitle] +``` + +## Debian GNU/Linux + +On Debian systems prefer the distribution packages unless you need per-project +virtual environments: + +- Zero-effort installation: automatically starts multiple instances based on + configs in `/etc/gunicorn.d`. +- Sensible log locations (`/var/log/gunicorn`) with `logrotate` support. +- Improved security: run each instance with a dedicated UNIX user/group. +- Safe upgrades: minimal downtime, reversible changes, and easy package purge. + +### stable ("buster") + +The Debian [stable](https://www.debian.org/releases/stable/) release ships +Gunicorn 19.9.0 (December 2020): + +```bash +sudo apt-get install gunicorn3 +``` + +Install Gunicorn 20.0.4 from [Debian Backports](https://backports.debian.org/) +by adding this line to `/etc/apt/sources.list`: + +```text +deb http://ftp.debian.org/debian buster-backports main +``` + +Refresh package metadata and install: + +```bash +sudo apt-get update +sudo apt-get -t buster-backports install gunicorn +``` + +### oldstable ("stretch") + +Stretch provides Gunicorn 19.6.0 (December 2020). Install the Python 3 version: + +```bash +sudo apt-get install gunicorn3 +``` + +To upgrade to 19.7.1 from backports, add: + +```text +deb http://ftp.debian.org/debian stretch-backports main +``` + +Then update and install: + +```bash +sudo apt-get update +sudo apt-get -t stretch-backports install gunicorn3 +``` + +### testing ("bullseye") and unstable ("sid") + +Both distributions include Gunicorn 20.0.4. Install it in the usual way: + +```bash +sudo apt-get install gunicorn +``` + +## Ubuntu + +Ubuntu 20.04 LTS (Focal Fossa) and newer include Gunicorn 20.0.4. Keep it +current through the package manager: + +```bash +sudo apt-get update +sudo apt-get install gunicorn +``` 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..1b7f0722 --- /dev/null +++ b/docs/content/news.md @@ -0,0 +1,75 @@ + +# Changelog + +## 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 + +- [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/reference/settings.md b/docs/content/reference/settings.md new file mode 100644 index 00000000..d320fcf4 --- /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..7cf82930 --- /dev/null +++ b/docs/content/styles/overrides.css @@ -0,0 +1,192 @@ +:root { + --gunicorn-green: #1d692d; + --gunicorn-green-dark: #14501f; + --gunicorn-green-light: #2a8729; + --gunicorn-cream: #f6f6f1; + --md-primary-fg-color: var(--gunicorn-green-light); + --md-primary-fg-color--light: #3da843; + --md-primary-fg-color--dark: var(--gunicorn-green-dark); + --md-accent-fg-color: var(--gunicorn-green); +} + +[data-md-color-scheme="slate"] { + --gunicorn-cream: #1d1f1d; + --md-primary-fg-color: var(--gunicorn-green); + --md-primary-fg-color--light: #3da843; + --md-primary-fg-color--dark: var(--gunicorn-green-dark); + --md-accent-fg-color: var(--gunicorn-green-light); +} + +.md-header__button.md-logo svg { + height: 1.8rem; +} + +.md-typeset .hero { + margin: 2rem 0 3rem; + padding: 3.5rem; + background: linear-gradient(135deg, rgba(29, 105, 45, 0.96), rgba(42, 135, 41, 0.85)); + color: #fff; + border-radius: 18px; + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.12); +} + +[data-md-color-scheme="slate"] .md-typeset .hero { + background: linear-gradient(135deg, rgba(20, 80, 31, 0.95), rgba(29, 105, 45, 0.88)); + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4); +} + +.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: 700; + line-height: 1.2; +} + +.md-typeset .hero__cta { + margin-top: 1.75rem; + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.md-typeset .hero__code { + flex: 1 1 260px; + max-width: 420px; + background: rgba(255, 255, 255, 0.08); + border-radius: 14px; + padding: 1.5rem; + backdrop-filter: blur(4px); + font-size: 0.95rem; +} + +[data-md-color-scheme="slate"] .md-typeset .hero__code { + background: rgba(0, 0, 0, 0.35); +} + +.md-typeset .hero__code pre { + margin: 0 0 1rem; + border: none; + background: rgba(0, 0, 0, 0.35); + color: #e8f5ea; +} + +[data-md-color-scheme="slate"] .md-typeset .hero__code pre { + background: rgba(0, 0, 0, 0.55); +} + +.md-typeset .hero__version { + font-weight: 600; + letter-spacing: 0.01em; +} + +.md-typeset .feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1.6rem; + margin: 2.5rem 0 3rem; +} + +.md-typeset .feature-card { + background: var(--gunicorn-cream); + border-radius: 14px; + padding: 1.5rem; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +[data-md-color-scheme="slate"] .md-typeset .feature-card { + background: rgba(45, 48, 45, 0.9); + border: 1px solid rgba(255, 255, 255, 0.05); + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.35); +} + +.md-typeset .feature-card h3 { + margin-top: 0; + font-size: 1.3rem; + color: var(--gunicorn-green-dark); +} + +[data-md-color-scheme="slate"] .md-typeset .feature-card h3 { + color: var(--gunicorn-cream); +} + +.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:hover { + transform: translateY(-4px); + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.12); +} + +.md-typeset .feature-card:hover a::after { + content: '\\2192'; + opacity: 1; + transform: translateX(4px); +} + +.md-typeset .feature-card a::after { + content: '\\2192'; + opacity: 0; + transition: opacity 0.2s ease, transform 0.2s ease; + transform: translateX(0); +} + +@media (max-width: 960px) { + .md-typeset .hero { + padding: 2.25rem; + } + + .md-typeset .hero__copy h1 { + font-size: 2.2rem; + } +} + +@media (max-width: 720px) { + .md-typeset .hero { + margin-top: 1.5rem; + padding: 1.75rem; + } + + .md-typeset .hero__cta { + flex-direction: column; + align-items: stretch; + } + + .md-typeset .hero__code { + width: 100%; + } +} + +.md-footer-meta__inner { + flex-wrap: wrap; +} + +.md-typeset .hero__logo { + height: 64px; + margin-bottom: 1.25rem; +} + +[data-md-color-scheme="slate"] .md-typeset .hero__logo { + filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.35)); +} 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..d6945086 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,94 @@ +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 + - Guides: + - Install: install.md + - Run: run.md + - Configure: configure.md + - Deploy: deploy.md + - Signals: signals.md + - Instrumentation: instrumentation.md + - Custom: custom.md + - Community: community.md + - FAQ: faq.md + - Design: design.md + - Reference: + - Settings: reference/settings.md + - News: + - Latest: 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 + language: en + logo: assets/gunicorn.svg + favicon: assets/gunicorn.svg + features: + - content.code.copy + - navigation.instant + - navigation.tracking + - navigation.sections + - navigation.tabs + - navigation.top + - search.highlight + - search.suggest + - toc.follow + +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 + +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/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..eb370f09 --- /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" }, +]