Bug 2002002 - Stop using vendored taskcluster-taskgraph, r=firefox-build-system-reviewers,taskgraph-reviewers,releng-reviewers,mach-reviewers,ahal,ahochheiden

Differential Revision: https://phabricator.services.mozilla.com/D273841
This commit is contained in:
abhishekmadan30
2026-02-02 20:09:02 +00:00
committed by amadan@mozilla.com
parent 7f33921416
commit fff9fde7e9
138 changed files with 8626 additions and 5073 deletions
-1
View File
@@ -229,7 +229,6 @@ tasks:
# This causes cached_task digest generation to be random for
# some tasks. Disable bytecode generation to work around that.
PYTHONDONTWRITEBYTECODE: '1'
MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE: 'system'
- $if: 'tasks_for == "action"'
then:
ACTION_TASK_GROUP_ID: '${action.taskGroupId}' # taskGroupId of the target task
+1
View File
@@ -3,6 +3,7 @@
[include]
# List of dependencies for the command
path:python/sites/docs.txt
path:taskcluster/requirements.txt
# Code for generating docs.
glob:docs/**
+19 -15
View File
@@ -12,16 +12,13 @@ import pathlib
import shutil
from collections import OrderedDict
# As a result of the selective module loading changes, this import has to be
# done here. It is not explicitly used, but it has an implicit side-effect
# (bringing in TASKCLUSTER_ROOT_URL) which is necessary.
import gecko_taskgraph.main # noqa: F401
import mozversioncontrol
from mach.decorators import Command, CommandArgument, SubCommand
from mozbuild.artifact_builds import JOB_CHOICES
from mozbuild.base import MachCommandConditions as conditions
from mozbuild.dirutils import ensureParentDir
from mozbuild.util import get_root_url, get_taskcluster_client
_COULD_NOT_FIND_ARTIFACTS_TEMPLATE = (
"ERROR!!!!!! Could not find artifacts for a toolchain build named "
@@ -290,7 +287,6 @@ def artifact_toolchain(
import redo
import requests
from taskgraph.util.taskcluster import get_artifact_url
from mozbuild.action.tooltool import FileRecord, open_manifest, unpack_file
from mozbuild.artifacts import ArtifactCache
@@ -316,9 +312,8 @@ def artifact_toolchain(
cache_dir = os.path.join(command_context._mach_context.state_dir, "toolchains")
tooltool_host = os.environ.get("TOOLTOOL_HOST", "tooltool.mozilla-releng.net")
taskcluster_proxy_url = os.environ.get("TASKCLUSTER_PROXY_URL")
if taskcluster_proxy_url:
tooltool_url = f"{taskcluster_proxy_url}/{tooltool_host}"
if "TASKCLUSTER_PROXY_URL" in os.environ:
tooltool_url = f"{get_root_url()}/{tooltool_host}"
else:
tooltool_url = f"https://{tooltool_host}"
@@ -343,10 +338,12 @@ def artifact_toolchain(
class ArtifactRecord(DownloadRecord):
def __init__(self, task_id, artifact_name):
queue = get_taskcluster_client("queue")
for _ in redo.retrier(attempts=retry + 1, sleeptime=60):
cot = cache._download_manager.session.get(
get_artifact_url(task_id, "public/chain-of-trust.json")
cot_url = queue.buildUrl(
"getLatestArtifact", task_id, "public/chain-of-trust.json"
)
cot = cache._download_manager.session.get(cot_url)
if cot.status_code >= 500:
continue
cot.raise_for_status()
@@ -362,11 +359,18 @@ def artifact_toolchain(
pass
name = os.path.basename(artifact_name)
artifact_url = get_artifact_url(
task_id,
artifact_name,
use_proxy=not artifact_name.startswith("public/"),
)
if (
not artifact_name.startswith("public/")
and "TASKCLUSTER_PROXY_URL" in os.environ
):
artifact_url = queue.buildUrl(
"getLatestArtifact", task_id, artifact_name
)
else:
public_queue = get_taskcluster_client("queue", block_proxy=True)
artifact_url = public_queue.buildUrl(
"getLatestArtifact", task_id, artifact_name
)
super().__init__(artifact_url, name, None, digest, algorithm, unpack=True)
records = OrderedDict()
+19 -8
View File
@@ -59,13 +59,11 @@ from mozpack import executables
from mozpack.files import FileFinder, JarFinder, TarFinder
from mozpack.mozjar import JarReader, JarWriter
from mozpack.packager.unpack import UnpackFinder
from taskcluster.exceptions import TaskclusterRestFailure
from taskgraph.util.taskcluster import find_task_id, get_artifact_url, list_artifacts
from mozbuild.artifact_builds import JOB_CHOICES
from mozbuild.artifact_cache import ArtifactCache
from mozbuild.dirutils import ensureParentDir, mkdir
from mozbuild.util import FileAvoidWrite
from mozbuild.util import FileAvoidWrite, get_root_url, get_taskcluster_client
# Number of candidate pushheads to cache per parent changeset.
NUM_PUSHHEADS_TO_QUERY_PER_PARENT = 50
@@ -1173,8 +1171,13 @@ class TaskCache(CacheManager):
{"namespace": namespace},
"Searching Taskcluster index with namespace: {namespace}",
)
from taskcluster.exceptions import TaskclusterRestFailure
try:
taskId = find_task_id(namespace)
index = get_taskcluster_client("index")
task = index.findTask(namespace)
taskId = task["taskId"]
except (KeyError, TaskclusterRestFailure) as e:
if isinstance(e, TaskclusterRestFailure) and e.status_code != 404:
raise
@@ -1183,7 +1186,9 @@ class TaskCache(CacheManager):
# care about; and even those that do may not have completed yet.
raise ValueError(f"Task for {namespace} does not exist (yet)!")
return taskId, list_artifacts(taskId)
queue = get_taskcluster_client("queue")
response = queue.listLatestArtifacts(taskId)
return taskId, response["artifacts"]
class Artifacts:
@@ -1593,7 +1598,9 @@ https://firefox-source-docs.mozilla.org/contributing/vcs/mercurial_bundles.html
urls = []
for artifact_name in self._artifact_job.find_candidate_artifacts(artifacts):
url = get_artifact_url(taskId, artifact_name)
url = (
f"{get_root_url()}/api/queue/v1/task/{taskId}/artifacts/{artifact_name}"
)
urls.append(url)
if urls:
self.log(
@@ -1809,11 +1816,15 @@ https://firefox-source-docs.mozilla.org/contributing/vcs/mercurial_bundles.html
return self._install_from_hg_pushheads(pushheads, distdir)
def install_from_task(self, taskId, distdir):
artifacts = list_artifacts(taskId)
queue = get_taskcluster_client("queue")
response = queue.listLatestArtifacts(taskId)
artifacts = response["artifacts"]
urls = []
for artifact_name in self._artifact_job.find_candidate_artifacts(artifacts):
url = get_artifact_url(taskId, artifact_name)
url = (
f"{get_root_url()}/api/queue/v1/task/{taskId}/artifacts/{artifact_name}"
)
urls.append(url)
if not urls:
raise ValueError(f"Task {taskId} existed, but no artifacts found!")
+26
View File
@@ -1407,3 +1407,29 @@ def ensure_l10n_central(command_context):
raise NotAGitRepositoryError(
f"Directory is not a git repository: {l10n_base_dir}"
)
# Taskcluster API root URL (Firefox's production instance)
TASKCLUSTER_ROOT_URL = "https://firefox-ci-tc.services.mozilla.com"
def get_root_url(block_proxy=False):
if "TASKCLUSTER_PROXY_URL" in os.environ and not block_proxy:
return os.environ["TASKCLUSTER_PROXY_URL"].rstrip("/")
if "TASKCLUSTER_ROOT_URL" in os.environ:
return os.environ["TASKCLUSTER_ROOT_URL"].rstrip("/")
return TASKCLUSTER_ROOT_URL
def get_taskcluster_client(service: str, block_proxy=False):
import taskcluster
if "TASKCLUSTER_PROXY_URL" in os.environ and not block_proxy:
options = {"rootUrl": os.environ["TASKCLUSTER_PROXY_URL"].rstrip("/")}
else:
options = taskcluster.optionsFromEnvironment({
"rootUrl": get_root_url(block_proxy)
})
return getattr(taskcluster, service[0].upper() + service[1:])(options)
+1 -28
View File
@@ -1,5 +1,6 @@
requires-python:>=3.9
pth:third_party/python/vsdownload
pypi-optional:taskcluster-taskgraph==18.0.3:toolchain artifacts will not be bootstrapped
vendored:testing/web-platform/tests/tools/third_party/h2/src
vendored:testing/web-platform/tests/tools/third_party/hpack/src
vendored:testing/web-platform/tests/tools/third_party/html5lib
@@ -8,53 +9,25 @@ vendored:testing/web-platform/tests/tools/third_party/pywebsocket3
vendored:testing/web-platform/tests/tools/third_party/webencodings
vendored:testing/web-platform/tests/tools/wptrunner
vendored:testing/web-platform/tests/tools/wptserve
vendored:third_party/python/aiohappyeyeballs
vendored:third_party/python/aiohttp
vendored:third_party/python/aiosignal
vendored:third_party/python/arrow
vendored:third_party/python/async_timeout
vendored:third_party/python/binaryornot
vendored:third_party/python/compare_locales
vendored:third_party/python/cookiecutter
vendored:third_party/python/dlmanager
vendored:third_party/python/ecdsa
vendored:third_party/python/fluent.migrate
vendored:third_party/python/fluent.syntax
vendored:third_party/python/frozenlist
vendored:third_party/python/gitignorant
vendored:third_party/python/giturlparse
vendored:third_party/python/gyp/pylib
vendored:third_party/python/iniparse
vendored:third_party/python/json_e
vendored:third_party/python/mako
vendored:third_party/python/markdown_it_py
vendored:third_party/python/mdurl
vendored:third_party/python/mohawk
vendored:third_party/python/moz_l10n
vendored:third_party/python/mozilla_repo_urls
vendored:third_party/python/mozilla_taskgraph
vendored:third_party/python/multidict
vendored:third_party/python/pathspec
vendored:third_party/python/ply
vendored:third_party/python/polib
vendored:third_party/python/propcache
vendored:third_party/python/pyasn1
vendored:third_party/python/pyasn1_modules
vendored:third_party/python/pygments
vendored:third_party/python/pylru
vendored:third_party/python/python_dateutil
vendored:third_party/python/python_slugify
vendored:third_party/python/pyyaml/lib/
vendored:third_party/python/redo
vendored:third_party/python/requests_unixsocket
vendored:third_party/python/rich
vendored:third_party/python/rsa
vendored:third_party/python/slugid
vendored:third_party/python/taskcluster
vendored:third_party/python/taskcluster_taskgraph
vendored:third_party/python/taskcluster_urls
vendored:third_party/python/text_unidecode
vendored:third_party/python/types_python_dateutil
vendored:third_party/python/voluptuous
vendored:third_party/python/yamllint
vendored:third_party/python/yarl
-13
View File
@@ -10,11 +10,7 @@ vendored:testing/web-platform/tests/tools/wptserve
vendored:third_party/python/aiohappyeyeballs
vendored:third_party/python/aiohttp
vendored:third_party/python/aiosignal
vendored:third_party/python/arrow
vendored:third_party/python/async_timeout
vendored:third_party/python/binaryornot
vendored:third_party/python/compare_locales
vendored:third_party/python/cookiecutter
vendored:third_party/python/cookies
vendored:third_party/python/dlmanager
vendored:third_party/python/ecdsa
@@ -25,33 +21,24 @@ vendored:third_party/python/frozenlist
vendored:third_party/python/giturlparse
vendored:third_party/python/gyp/pylib
vendored:third_party/python/json_e
vendored:third_party/python/markdown_it_py
vendored:third_party/python/mdurl
vendored:third_party/python/mohawk
vendored:third_party/python/mozilla_repo_urls
vendored:third_party/python/mozilla_taskgraph
vendored:third_party/python/multidict
vendored:third_party/python/pathspec
vendored:third_party/python/ply
vendored:third_party/python/propcache
vendored:third_party/python/pyasn1
vendored:third_party/python/pyasn1_modules
vendored:third_party/python/pygments
vendored:third_party/python/pylru
vendored:third_party/python/python_dateutil
vendored:third_party/python/python_slugify
vendored:third_party/python/pyyaml/lib/
vendored:third_party/python/redo
vendored:third_party/python/requests_unixsocket
vendored:third_party/python/responses
vendored:third_party/python/rich
vendored:third_party/python/rsa
vendored:third_party/python/slugid
vendored:third_party/python/taskcluster
vendored:third_party/python/taskcluster_taskgraph
vendored:third_party/python/taskcluster_urls
vendored:third_party/python/text_unidecode
vendored:third_party/python/types_python_dateutil
vendored:third_party/python/voluptuous
vendored:third_party/python/yamllint
vendored:third_party/python/yarl
+1 -10
View File
@@ -21,13 +21,10 @@ pypi:sphinx-markdown-tables==0.0.17
pypi:sphinx-rtd-theme==2.0.0
pypi:sphinx-tabs==3.4.7
pypi:sphinxcontrib-mermaid==1.0.0
requirements-txt:taskcluster/requirements.txt
vendored:third_party/python/aiohappyeyeballs
vendored:third_party/python/aiohttp
vendored:third_party/python/aiosignal
vendored:third_party/python/arrow
vendored:third_party/python/async_timeout
vendored:third_party/python/binaryornot
vendored:third_party/python/cookiecutter
vendored:third_party/python/dlmanager
vendored:third_party/python/fluent.migrate
vendored:third_party/python/fluent.syntax
@@ -35,21 +32,15 @@ vendored:third_party/python/frozenlist
vendored:third_party/python/giturlparse
vendored:third_party/python/gyp/pylib
vendored:third_party/python/json_e
vendored:third_party/python/markdown_it_py
vendored:third_party/python/mdurl
vendored:third_party/python/mohawk
vendored:third_party/python/mozilla_repo_urls
vendored:third_party/python/propcache
vendored:third_party/python/pygments
vendored:third_party/python/pylru
vendored:third_party/python/pyyaml/lib/
vendored:third_party/python/redo
vendored:third_party/python/requests_unixsocket
vendored:third_party/python/responses
vendored:third_party/python/rich
vendored:third_party/python/slugid
vendored:third_party/python/taskcluster
vendored:third_party/python/taskcluster_taskgraph
vendored:third_party/python/taskcluster_urls
vendored:third_party/python/types_python_dateutil
vendored:third_party/python/voluptuous
+2 -14
View File
@@ -21,40 +21,28 @@ pypi:sphinxcontrib-htmlhelp==2.0.1
pypi:sphinxcontrib-mermaid==1.0.0
pypi:tox==2.7.0
pypi:virtualenv==20.24.7
requirements-txt:taskcluster/requirements.txt
vendored:third_party/python/aiohappyeyeballs
vendored:third_party/python/aiohttp
vendored:third_party/python/aiosignal
vendored:third_party/python/arrow
vendored:third_party/python/async_timeout
vendored:third_party/python/binaryornot
vendored:third_party/python/compare_locales
vendored:third_party/python/cookiecutter
vendored:third_party/python/esprima
vendored:third_party/python/fluent.syntax
vendored:third_party/python/frozenlist
vendored:third_party/python/giturlparse
vendored:third_party/python/json_e
vendored:third_party/python/markdown_it_py
vendored:third_party/python/mdurl
vendored:third_party/python/mohawk
vendored:third_party/python/mozilla_repo_urls
vendored:third_party/python/mozilla_taskgraph
vendored:third_party/python/multidict
vendored:third_party/python/pathspec
vendored:third_party/python/propcache
vendored:third_party/python/pygments
vendored:third_party/python/python_dateutil
vendored:third_party/python/python_slugify
vendored:third_party/python/pyyaml/lib/
vendored:third_party/python/redo
vendored:third_party/python/requests_unixsocket
vendored:third_party/python/rich
vendored:third_party/python/slugid
vendored:third_party/python/taskcluster
vendored:third_party/python/taskcluster_taskgraph
vendored:third_party/python/taskcluster_urls
vendored:third_party/python/text_unidecode
vendored:third_party/python/types_python_dateutil
vendored:third_party/python/voluptuous
vendored:third_party/python/yamllint
vendored:third_party/python/yarl
vendored:third_party/python/yarl
+3 -3
View File
@@ -63,18 +63,18 @@ pypi-optional:orjson>=3.10:json operations will be slower in various tools
# Mach gracefully handles the case where `psutil` is unavailable.
# We aren't (yet) able to pin packages in automation, so we have to
# support down to the oldest locally-installed version (5.4.2).
pypi-optional:psutil>=5.4.2:telemetry will be missing some data
pypi-optional:psutil>=5.4.2,<=5.9.4:telemetry will be missing some data
pypi-optional:rtoml>=0.11.0:toml operations will be slower in various tools
pypi-optional:zstandard>=0.11.1,<=0.24.0:zstd archives will not be possible to extract
pypi-optional:zstandard>=0.11.1,<=0.25.0:zstd archives will not be possible to extract
vendored-fallback:pyyaml:third_party/python/pyyaml/lib/:faster native loading is disabled
vendored:third_party/python/ansicon
vendored:third_party/python/appdirs
vendored:third_party/python/async_timeout
vendored:third_party/python/attrs
vendored:third_party/python/blessed
vendored:third_party/python/build
vendored:third_party/python/cbor2
vendored:third_party/python/certifi
vendored:third_party/python/chardet
vendored:third_party/python/charset_normalizer
vendored:third_party/python/click
vendored:third_party/python/colorama
+1 -3
View File
@@ -2,16 +2,14 @@ requires-python:>=3.9
pypi:coverage==5.1
pypi:pytest-asyncio==1.2.0
pypi:pytest==8.4.2
requirements-txt:taskcluster/requirements.txt
vendored:third_party/python/dlmanager
vendored:third_party/python/esprima
vendored:third_party/python/giturlparse
vendored:third_party/python/mozilla_repo_urls
vendored:third_party/python/mozilla_taskgraph
vendored:third_party/python/mozilla_taskgraph
vendored:third_party/python/pyyaml/lib/
vendored:third_party/python/redo
vendored:third_party/python/responses
vendored:third_party/python/slugid
vendored:third_party/python/taskcluster_taskgraph
vendored:third_party/python/taskcluster_urls
vendored:third_party/python/voluptuous
+2 -14
View File
@@ -20,7 +20,8 @@ pypi:sphinxcontrib-applehelp==1.0.4
pypi:sphinxcontrib-htmlhelp==2.0.1
pypi:sphinxcontrib-mermaid==1.0.0
pypi:werkzeug==2.3.8
pypi:zstandard==0.24.0
pypi:zstandard==0.25.0
requirements-txt:taskcluster/requirements.txt
vendored:testing/web-platform/tests/tools/third_party/h2/src
vendored:testing/web-platform/tests/tools/third_party/hpack/src
vendored:testing/web-platform/tests/tools/third_party/html5lib
@@ -32,11 +33,7 @@ vendored:testing/web-platform/tests/tools/wptserve
vendored:third_party/python/aiohappyeyeballs
vendored:third_party/python/aiohttp
vendored:third_party/python/aiosignal
vendored:third_party/python/arrow
vendored:third_party/python/async_timeout
vendored:third_party/python/binaryornot
vendored:third_party/python/compare_locales
vendored:third_party/python/cookiecutter
vendored:third_party/python/cookies
vendored:third_party/python/dlmanager
vendored:third_party/python/ecdsa
@@ -47,33 +44,24 @@ vendored:third_party/python/frozenlist
vendored:third_party/python/giturlparse
vendored:third_party/python/gyp/pylib
vendored:third_party/python/json_e
vendored:third_party/python/markdown_it_py
vendored:third_party/python/mdurl
vendored:third_party/python/mohawk
vendored:third_party/python/mozilla_repo_urls
vendored:third_party/python/mozilla_taskgraph
vendored:third_party/python/multidict
vendored:third_party/python/pathspec
vendored:third_party/python/ply
vendored:third_party/python/propcache
vendored:third_party/python/pyasn1
vendored:third_party/python/pyasn1_modules
vendored:third_party/python/pygments
vendored:third_party/python/pylru
vendored:third_party/python/python_dateutil
vendored:third_party/python/python_slugify
vendored:third_party/python/pyyaml/lib/
vendored:third_party/python/redo
vendored:third_party/python/requests_unixsocket
vendored:third_party/python/responses
vendored:third_party/python/rich
vendored:third_party/python/rsa
vendored:third_party/python/slugid
vendored:third_party/python/taskcluster
vendored:third_party/python/taskcluster_taskgraph
vendored:third_party/python/taskcluster_urls
vendored:third_party/python/text_unidecode
vendored:third_party/python/types_python_dateutil
vendored:third_party/python/voluptuous
vendored:third_party/python/yamllint
vendored:third_party/python/yarl
+12
View File
@@ -0,0 +1,12 @@
requires-python:>=3.9
requirements-txt:taskcluster/requirements.txt
vendored:testing/web-platform/tests/tools/third_party/h2/src
vendored:testing/web-platform/tests/tools/third_party/hpack/src
vendored:testing/web-platform/tests/tools/third_party/html5lib
vendored:testing/web-platform/tests/tools/third_party/hyperframe/src
vendored:testing/web-platform/tests/tools/third_party/pywebsocket3
vendored:testing/web-platform/tests/tools/third_party/webencodings
vendored:testing/web-platform/tests/tools/wptrunner
vendored:testing/web-platform/tests/tools/wptserve
vendored:third_party/python/fluent.syntax
vendored:third_party/python/pathspec
+2 -14
View File
@@ -2,6 +2,7 @@ requires-python:>=3.9
pypi:Flask==2.1.3
pypi:auth0-python==4.4.1
pypi:werkzeug==2.3.8
requirements-txt:taskcluster/requirements.txt
vendored:testing/web-platform/tests/tools/third_party/h2/src
vendored:testing/web-platform/tests/tools/third_party/hpack/src
vendored:testing/web-platform/tests/tools/third_party/html5lib
@@ -13,11 +14,7 @@ vendored:testing/web-platform/tests/tools/wptserve
vendored:third_party/python/aiohappyeyeballs
vendored:third_party/python/aiohttp
vendored:third_party/python/aiosignal
vendored:third_party/python/arrow
vendored:third_party/python/async_timeout
vendored:third_party/python/binaryornot
vendored:third_party/python/compare_locales
vendored:third_party/python/cookiecutter
vendored:third_party/python/cookies
vendored:third_party/python/dlmanager
vendored:third_party/python/ecdsa
@@ -28,33 +25,24 @@ vendored:third_party/python/frozenlist
vendored:third_party/python/giturlparse
vendored:third_party/python/gyp/pylib
vendored:third_party/python/json_e
vendored:third_party/python/markdown_it_py
vendored:third_party/python/mdurl
vendored:third_party/python/mohawk
vendored:third_party/python/mozilla_repo_urls
vendored:third_party/python/mozilla_taskgraph
vendored:third_party/python/multidict
vendored:third_party/python/pathspec
vendored:third_party/python/ply
vendored:third_party/python/propcache
vendored:third_party/python/pyasn1
vendored:third_party/python/pyasn1_modules
vendored:third_party/python/pygments
vendored:third_party/python/pylru
vendored:third_party/python/python_dateutil
vendored:third_party/python/python_slugify
vendored:third_party/python/pyyaml/lib/
vendored:third_party/python/redo
vendored:third_party/python/requests_unixsocket
vendored:third_party/python/responses
vendored:third_party/python/rich
vendored:third_party/python/rsa
vendored:third_party/python/slugid
vendored:third_party/python/taskcluster
vendored:third_party/python/taskcluster_taskgraph
vendored:third_party/python/taskcluster_urls
vendored:third_party/python/text_unidecode
vendored:third_party/python/types_python_dateutil
vendored:third_party/python/voluptuous
vendored:third_party/python/yamllint
vendored:third_party/python/yarl
vendored:third_party/python/yarl
+1 -3
View File
@@ -1,6 +1,7 @@
requires-python:>=3.9
pth:taskcluster/gecko_taskgraph
pypi:psutil==5.9.4
requirements-txt:taskcluster/requirements.txt
vendored:testing/web-platform/tests/tools/third_party/h2/src
vendored:testing/web-platform/tests/tools/third_party/hpack/src
vendored:testing/web-platform/tests/tools/third_party/html5lib
@@ -10,15 +11,12 @@ vendored:testing/web-platform/tests/tools/third_party/webencodings
vendored:testing/web-platform/tests/tools/wptrunner
vendored:testing/web-platform/tests/tools/wptserve
vendored:third_party/python/aiohttp
vendored:third_party/python/async_timeout
vendored:third_party/python/giturlparse
vendored:third_party/python/mohawk
vendored:third_party/python/mozilla_repo_urls
vendored:third_party/python/mozilla_taskgraph
vendored:third_party/python/python_dateutil
vendored:third_party/python/redo
vendored:third_party/python/slugid
vendored:third_party/python/taskcluster
vendored:third_party/python/taskcluster_taskgraph
vendored:third_party/python/taskcluster_urls
vendored:third_party/python/voluptuous
+2 -4
View File
@@ -62,10 +62,8 @@ COPY --chown=worker topsrcdir/taskcluster/docker/recipes/dot-config /builds/work
# %include taskcluster/scripts/run-task
COPY topsrcdir/taskcluster/scripts/run-task /builds/worker/bin/run-task-hg
# %include third_party/python/taskcluster_taskgraph/taskgraph/run-task/run-task
COPY topsrcdir/third_party/python/taskcluster_taskgraph/taskgraph/run-task/run-task /builds/worker/bin/run-task-git
# %include third_party/python/taskcluster_taskgraph/taskgraph/run-task/fetch-content
ADD topsrcdir/third_party/python/taskcluster_taskgraph/taskgraph/run-task/fetch-content /builds/worker/bin/fetch-content
# Setup upstream `run-task` and `fetch-content`
# %include-run-task
RUN chown -R worker:worker /builds/worker/bin && chmod 755 /builds/worker/bin/*
+1 -5
View File
@@ -8,12 +8,8 @@ apt-get update
apt-get install \
python-is-python3 \
sudo \
python3-yaml \
python3-pip
python3-yaml
pip install --break-system-packages --disable-pip-version-check --quiet --no-cache-dir orjson==3.10.15 rtoml==0.13.0
apt-get remove --purge python3-pip
apt-get autoremove --purge
apt-get clean
apt-get autoclean
+1 -4
View File
@@ -28,10 +28,7 @@ RUN /usr/local/sbin/setup_packages.sh $TASKCLUSTER_ROOT_URL $DOCKER_IMAGE_PACKAG
# %include taskcluster/scripts/run-task
COPY topsrcdir/taskcluster/scripts/run-task /builds/worker/bin/run-task-hg
# %include third_party/python/taskcluster_taskgraph/taskgraph/run-task/run-task
COPY topsrcdir/third_party/python/taskcluster_taskgraph/taskgraph/run-task/run-task /builds/worker/bin/run-task-git
# %include third_party/python/taskcluster_taskgraph/taskgraph/run-task/fetch-content
ADD topsrcdir/third_party/python/taskcluster_taskgraph/taskgraph/run-task/fetch-content /builds/worker/bin/fetch-content
# %include-run-task
RUN pip3 install redo==2.0.4 --break-system-packages
@@ -64,10 +64,7 @@ COPY topsrcdir/taskcluster/docker/recipes/dot-config /builds/worker/.config
# %include taskcluster/scripts/run-task
COPY topsrcdir/taskcluster/scripts/run-task /builds/worker/bin/run-task-hg
# %include third_party/python/taskcluster_taskgraph/taskgraph/run-task/run-task
COPY topsrcdir/third_party/python/taskcluster_taskgraph/taskgraph/run-task/run-task /builds/worker/bin/run-task-git
# %include third_party/python/taskcluster_taskgraph/taskgraph/run-task/fetch-content
ADD topsrcdir/third_party/python/taskcluster_taskgraph/taskgraph/run-task/fetch-content /builds/worker/bin/fetch-content
# %include-run-task
RUN chown -R worker:worker /builds/worker/bin && chmod 755 /builds/worker/bin/*
+4 -4
View File
@@ -31,10 +31,10 @@ documentation.
of being merged back together.
Today the version of Taskgraph under ``taskcluster/gecko_taskgraph`` depends
on the standalone version, which is vendored under
``third_party/python/taskcluster_taskgraph``. There is still a lot of
duplication between these places, but ``gecko_taskgraph`` is slowly being
re-written to consume standalone Taskgraph.
on the upstream version, which is installed as part of
``taskcluster/requirements.txt``. There is still a lot of duplication
between these places, but ``gecko_taskgraph`` is slowly being re-written to
consume upstream Taskgraph.
The ``taskcluster`` directory contains all the files needed to define the graph
of tasks that must be executed to build and test the Gecko tree. This is more
+15 -1
View File
@@ -10,7 +10,7 @@ from taskgraph import config as taskgraph_config
from taskgraph import generator
from taskgraph import morph as taskgraph_morph
from taskgraph.transforms.task import payload_builders
from taskgraph.util import schema
from taskgraph.util import docker, schema
from taskgraph.util import taskcluster as tc_util
from gecko_taskgraph.config import graph_config_schema
@@ -21,6 +21,20 @@ TEST_CONFIGS = os.path.join(GECKO, "taskcluster", "test_configs")
# Overwrite Taskgraph's default graph_config_schema with a custom one.
taskgraph_config.graph_config_schema = graph_config_schema
# Overwrite Taskgraph's RUN_TASK_SNIPPET to place the binaries in Gecko
# specific locations.
docker.RUN_TASK_FILES = {
f"run-task/{path}": os.path.join(docker.RUN_TASK_ROOT, path)
for path in [
"run-task",
"fetch-content",
]
}
docker.RUN_TASK_SNIPPET = [
"COPY run-task/run-task /builds/worker/bin/run-task-git\n",
"COPY run-task/fetch-content /builds/worker/bin/fetch-content\n",
]
# Don't use any of the upstream morphs.
# TODO Investigate merging our morphs with upstream.
taskgraph_morph.registered_morphs = []
+2 -3
View File
@@ -10,6 +10,7 @@ import time
from collections import defaultdict
from pathlib import Path
import taskgraph
import yaml
from redo import retry
from taskgraph import create
@@ -243,9 +244,7 @@ def taskgraph_decision(options, parameters=None):
# upload run-task, fetch-content, robustcheckout.py and more as artifacts
mozharness_dir = Path(GECKO, "testing", "mozharness")
scripts_dir = Path(GECKO, "taskcluster", "scripts")
taskgraph_dir = Path(
GECKO, "third_party", "python", "taskcluster_taskgraph", "taskgraph"
)
taskgraph_dir = Path(taskgraph.__file__).parent
to_copy = {
scripts_dir / "run-task": f"{ARTIFACTS_DIR}/run-task-hg",
scripts_dir / "tester" / "test-linux.sh": ARTIFACTS_DIR,
+1 -1
View File
@@ -356,7 +356,7 @@ def show_taskgraph(options):
].endswith(("taskgraph", "mozbuild")):
del sys.modules[mod]
# Ensure gecko_taskgraph is ahead of taskcluster_taskgraph in sys.path.
# Ensure gecko_taskgraph is ahead of upstream Taskgraph in sys.path.
# Without this, we may end up validating some things against the wrong
# schema.
import gecko_taskgraph # noqa
+1 -2
View File
@@ -267,8 +267,7 @@ def add_try_task_duplicates(taskgraph, label_to_taskid, parameters, graph_config
# this shim function exists so we can call it from the unittests.
# this works around an issue with
# third_party/python/taskcluster_taskgraph/taskgraph/morph.py#40
# this works around an issue with the morph in upstream Taskgraph
def _add_try_task_duplicates(taskgraph, label_to_taskid, parameters, graph_config):
try_config = parameters.get("try_task_config", {})
tasks = try_config.get("tasks", [])
+6 -12
View File
@@ -13,7 +13,9 @@ import hashlib
import os
import re
import time
from pathlib import Path
import taskgraph
from mozbuild.util import memoize
from mozilla_taskgraph.util.signed_artifacts import get_signed_artifacts
from taskcluster.utils import fromNow
@@ -43,16 +45,8 @@ from gecko_taskgraph.util.partners import get_partners_to_be_published
from gecko_taskgraph.util.scriptworker import BALROG_ACTIONS, get_release_config
from gecko_taskgraph.util.workertypes import get_worker_type, worker_type_implementation
RUN_TASK_HG = os.path.join(GECKO, "taskcluster", "scripts", "run-task")
RUN_TASK_GIT = os.path.join(
GECKO,
"third_party",
"python",
"taskcluster_taskgraph",
"taskgraph",
"run-task",
"run-task",
)
RUN_TASK_HG = Path(GECKO, "taskcluster", "scripts", "run-task")
RUN_TASK_GIT = Path(taskgraph.__file__).parent / "run-task" / "run-task"
SCCACHE_GCS_PROJECT = "sccache-3"
@@ -61,8 +55,8 @@ SCCACHE_GCS_PROJECT = "sccache-3"
def _run_task_suffix(repo_type):
"""String to append to cache names under control of run-task."""
if repo_type == "hg":
return hash_path(RUN_TASK_HG)[0:20]
return hash_path(RUN_TASK_GIT)[0:20]
return hash_path(str(RUN_TASK_HG))[0:20]
return hash_path(str(RUN_TASK_GIT))[0:20]
def _compute_geckoview_version(app_version, moz_build_date):
+3 -5
View File
@@ -58,7 +58,7 @@ firefox-ci:
- '.taskcluster.yml'
- 'taskcluster/kinds/**'
- 'taskcluster/**/*.py'
- 'third_party/python/taskcluster_taskgraph/**/*.py'
- 'taskcluster/requirements.txt'
- 'tools/tryselect/selectors/auto.py'
fog:
@@ -354,7 +354,7 @@ taskgraph-tests:
files-changed:
- 'python/mach/**/*.py'
- 'taskcluster/**/*.py'
- 'third_party/python/taskcluster_taskgraph/**/*.py'
- 'taskcluster/requirements.txt'
tryselect:
description: tools/tryselect unit tests
@@ -371,7 +371,7 @@ tryselect:
- 'taskcluster/config.yml'
- 'taskcluster/kinds/test/**'
- 'taskcluster/gecko_taskgraph/transforms/**'
- 'third_party/python/taskcluster_taskgraph/**/*.py'
- 'taskcluster/requirements.txt'
- 'tools/tryselect/**'
mozbuild:
@@ -649,8 +649,6 @@ verify-decision:
- .cron.yml
- .taskcluster.yml
- 'taskcluster/**'
- 'third_party/python/taskcluster/**'
- 'third_party/python/taskcluster_taskgraph/**'
webext:
description: WebExtensions python utilities unit tests
+1 -1
View File
@@ -42,4 +42,4 @@ diff:
files-changed:
- 'taskcluster/kinds/**'
- 'taskcluster/**/*.py'
- 'third_party/python/taskcluster_taskgraph/**/*.py'
- 'taskcluster/requirements.txt'
+22 -7
View File
@@ -13,17 +13,10 @@ import time
import traceback
from functools import partial
import gecko_taskgraph.main
from gecko_taskgraph.files_changed import get_locally_changed_files
from gecko_taskgraph.main import commands as taskgraph_commands
from mach.decorators import Command, CommandArgument, SubCommand
from mach.util import strtobool
from mozsystemmonitor.resourcemonitor import SystemResourceMonitor
# We're likely going to need the result of get_locally_changed_files, and it
# takes time to finish, so prefetch it as soon as possible.
get_locally_changed_files.preload(os.getcwd())
def setup_logging(command_context, quiet=False, verbose=True):
"""
@@ -58,6 +51,8 @@ def get_taskgraph_command_parser(name):
Returns:
ArgumentParser: An ArgumentParser instance.
"""
from gecko_taskgraph.main import commands as taskgraph_commands
command = taskgraph_commands[name]
parser = argparse.ArgumentParser()
for arg in command.func.args:
@@ -111,6 +106,7 @@ def get_taskgraph_decision_parser():
"taskgraph",
category="ci",
description="Manipulate TaskCluster task graphs defined in-tree",
virtualenv_name="taskgraph",
)
def taskgraph_command(command_context):
"""The taskgraph subcommands all relate to the generation of task graphs
@@ -127,6 +123,8 @@ def taskgraph_command(command_context):
parser=partial(get_taskgraph_command_parser, "kind-graph"),
)
def taskgraph_kind_graph(command_context, **options):
from gecko_taskgraph.main import commands as taskgraph_commands
try:
setup_logging(command_context)
return taskgraph_commands["kind-graph"].func(options)
@@ -196,6 +194,8 @@ def taskgraph_morphed(command_context, **options):
def run_show_taskgraph(command_context, **options):
import gecko_taskgraph.main
# There are cases where we don't want to set up mach logging (e.g logs
# are being redirected to disk). By monkeypatching the 'setup_logging'
# function we can let 'taskgraph.main' decide whether or not to log to
@@ -269,6 +269,8 @@ def taskgraph_decision(command_context, **options):
and requires a great many arguments. Commands like `mach taskgraph
optimized` are better suited to use on the command line, and can take
the parameters file generated by a decision task."""
from gecko_taskgraph.main import commands as taskgraph_commands
try:
setup_logging(command_context)
@@ -333,6 +335,8 @@ def taskgraph_decision(command_context, **options):
parser=partial(get_taskgraph_command_parser, "action-callback"),
)
def action_callback(command_context, **options):
from gecko_taskgraph.main import commands as taskgraph_commands
setup_logging(command_context)
taskgraph_commands["action-callback"].func(options)
@@ -344,6 +348,8 @@ def action_callback(command_context, **options):
parser=partial(get_taskgraph_command_parser, "test-action-callback"),
)
def test_action_callback(command_context, **options):
from gecko_taskgraph.main import commands as taskgraph_commands
setup_logging(command_context)
if not options["parameters"]:
@@ -360,6 +366,8 @@ def test_action_callback(command_context, **options):
parser=partial(get_taskgraph_command_parser, "load-image"),
)
def load_image(command_context, **kwargs):
from gecko_taskgraph.main import commands as taskgraph_commands
setup_logging(command_context)
taskgraph_commands["load-image"].func(kwargs)
@@ -371,6 +379,8 @@ def load_image(command_context, **kwargs):
parser=partial(get_taskgraph_command_parser, "build-image"),
)
def build_image(command_context, **kwargs):
from gecko_taskgraph.main import commands as taskgraph_commands
setup_logging(command_context)
try:
taskgraph_commands["build-image"].func(kwargs)
@@ -387,6 +397,8 @@ def build_image(command_context, **kwargs):
parser=partial(get_taskgraph_command_parser, "build-image"),
)
def image_digest(command_context, **kwargs):
from gecko_taskgraph.main import commands as taskgraph_commands
setup_logging(command_context)
taskgraph_commands["image-digest"].func(kwargs)
@@ -401,6 +413,8 @@ def image_digest(command_context, **kwargs):
parser=partial(get_taskgraph_command_parser, "load-task"),
)
def load_task(command_context, **kwargs):
from gecko_taskgraph.main import commands as taskgraph_commands
setup_logging(command_context)
taskgraph_commands["load-task"].func(kwargs)
@@ -409,6 +423,7 @@ def load_task(command_context, **kwargs):
"release-history",
category="ci",
description="Query balrog for release history used by enable partials generation",
virtualenv_name="try",
)
@CommandArgument(
"-b",
+8
View File
@@ -0,0 +1,8 @@
-c ../third_party/python/requirements.txt
mozilla-taskgraph==4.0.1
requests-unixsocket
taskcluster-taskgraph[orjson,load-image]==18.0.3
rtoml==0.13.0; python_version >= "3.10"
# Temporarily resolve conflict with myst-parser 2.0.0 in python/sites/docs.txt
markdown-it-py < 4
File diff suppressed because it is too large Load Diff
-10
View File
@@ -577,16 +577,6 @@ def fetch_artifacts():
print_line(b"fetches", b"fetching artifacts\n")
fetch_content = shutil.which("fetch-content")
if not fetch_content and os.environ.get("GECKO_PATH"):
fetch_content = os.path.join(
os.environ["GECKO_PATH"],
"third_party",
"python",
"taskcluster_taskgraph",
"taskgraph",
"run-task",
"fetch-content",
)
if not fetch_content or not os.path.isfile(fetch_content):
fetch_content = os.path.join(os.path.dirname(__file__), "fetch-content")
+1 -2
View File
@@ -6,8 +6,7 @@
#
# For each key, if it is a string the task's attribute must match exactly. If
# it is a list, the task's attribute must be a value contained in the list.
# For more information, see the `attrmatch` function in:
# https://searchfox.org/mozilla-central/source/third_party/python/taskcluster_taskgraph/taskgraph/util/attributes.py
# For more information, see the `attrmatch` function in `taskgraph.util.attributes`.
### Unittests
- kind: &unittest-kinds
@@ -183,6 +183,7 @@ class PlatformInfo:
filename = (
os.environ.get("GECKO_PATH", ".") + "/taskcluster/test_configs/variants.yml"
)
with open(filename) as f:
PlatformInfo.variant_data = yaml.safe_load(f.read())
+12 -1
View File
@@ -9,6 +9,7 @@ import subprocess
import sys
import tempfile
from dataclasses import dataclass
from enum import Enum
from os import environ, makedirs
from pathlib import Path
from shutil import copytree, unpack_archive
@@ -16,12 +17,22 @@ from shutil import copytree, unpack_archive
import mozinfo
import mozinstall
import requests
from gecko_taskgraph.transforms.update_test import ReleaseType
from mach.decorators import Command, CommandArgument
from mozbuild.base import BinaryNotFoundException
from mozlog.structured import commandline
from mozrelease.update_verify import UpdateVerifyConfig
class ReleaseType(Enum):
"""Release type - duplicated from gecko_taskgraph.transforms.update_test
to avoid importing taskgraph dependencies at mach command load time."""
release = 0
beta = 1
esr = 2
other = 3
STAGING_POLICY_PAYLOAD = {
"policies": {
"AppUpdateURL": "https://stage.balrog.nonprod.cloudops.mozgcp.net/update/6/Firefox/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml"
+450 -1
View File
@@ -10,6 +10,455 @@
.. towncrier release notes start
3.13.3 (2026-01-03)
===================
This release contains fixes for several vulnerabilities. It is advised to
upgrade as soon as possible.
Bug fixes
---------
- Fixed proxy authorization headers not being passed when reusing a connection, which caused 407 (Proxy authentication required) errors
-- by :user:`GLeurquin`.
*Related issues and pull requests on GitHub:*
:issue:`2596`.
- Fixed multipart reading failing when encountering an empty body part -- by :user:`Dreamsorcerer`.
*Related issues and pull requests on GitHub:*
:issue:`11857`.
- Fixed a case where the parser wasn't raising an exception for a websocket continuation frame when there was no initial frame in context.
*Related issues and pull requests on GitHub:*
:issue:`11862`.
Removals and backward incompatible breaking changes
---------------------------------------------------
- ``Brotli`` and ``brotlicffi`` minimum version is now 1.2.
Decompression now has a default maximum output size of 32MiB per decompress call -- by :user:`Dreamsorcerer`.
*Related issues and pull requests on GitHub:*
:issue:`11898`.
Packaging updates and notes for downstreams
-------------------------------------------
- Moved dependency metadata from :file:`setup.cfg` to :file:`pyproject.toml` per :pep:`621`
-- by :user:`cdce8p`.
*Related issues and pull requests on GitHub:*
:issue:`11643`.
Contributor-facing changes
--------------------------
- Removed unused ``update-pre-commit`` github action workflow -- by :user:`Cycloctane`.
*Related issues and pull requests on GitHub:*
:issue:`11689`.
Miscellaneous internal changes
------------------------------
- Optimized web server performance when access logging is disabled by reducing time syscalls -- by :user:`bdraco`.
*Related issues and pull requests on GitHub:*
:issue:`10713`.
- Added regression test for cached logging status -- by :user:`meehand`.
*Related issues and pull requests on GitHub:*
:issue:`11778`.
----
3.13.2 (2025-10-28)
===================
Bug fixes
---------
- Fixed cookie parser to continue parsing subsequent cookies when encountering a malformed cookie that fails regex validation, such as Google's ``g_state`` cookie with unescaped quotes -- by :user:`bdraco`.
*Related issues and pull requests on GitHub:*
:issue:`11632`.
- Fixed loading netrc credentials from the default :file:`~/.netrc` (:file:`~/_netrc` on Windows) location when the :envvar:`NETRC` environment variable is not set -- by :user:`bdraco`.
*Related issues and pull requests on GitHub:*
:issue:`11713`, :issue:`11714`.
- Fixed WebSocket compressed sends to be cancellation safe. Tasks are now shielded during compression to prevent compressor state corruption. This ensures that the stateful compressor remains consistent even when send operations are cancelled -- by :user:`bdraco`.
*Related issues and pull requests on GitHub:*
:issue:`11725`.
----
3.13.1 (2025-10-17)
===================
Features
--------
- Make configuration options in ``AppRunner`` also available in ``run_app()``
-- by :user:`Cycloctane`.
*Related issues and pull requests on GitHub:*
:issue:`11633`.
Bug fixes
---------
- Switched to `backports.zstd` for Python <3.14 and fixed zstd decompression for chunked zstd streams -- by :user:`ZhaoMJ`.
Note: Users who installed ``zstandard`` for support on Python <3.14 will now need to install
``backports.zstd`` instead (installing ``aiohttp[speedups]`` will do this automatically).
*Related issues and pull requests on GitHub:*
:issue:`11623`.
- Updated ``Content-Type`` header parsing to return ``application/octet-stream`` when header contains invalid syntax.
See :rfc:`9110#section-8.3-5`.
-- by :user:`sgaist`.
*Related issues and pull requests on GitHub:*
:issue:`10889`.
- Fixed Python 3.14 support when built without ``zstd`` support -- by :user:`JacobHenner`.
*Related issues and pull requests on GitHub:*
:issue:`11603`.
- Fixed blocking I/O in the event loop when using netrc authentication by moving netrc file lookup to an executor -- by :user:`bdraco`.
*Related issues and pull requests on GitHub:*
:issue:`11634`.
- Fixed routing to a sub-application added via ``.add_domain()`` not working
if the same path exists on the parent app. -- by :user:`Dreamsorcerer`.
*Related issues and pull requests on GitHub:*
:issue:`11673`.
Packaging updates and notes for downstreams
-------------------------------------------
- Moved core packaging metadata from :file:`setup.cfg` to :file:`pyproject.toml` per :pep:`621`
-- by :user:`cdce8p`.
*Related issues and pull requests on GitHub:*
:issue:`9951`.
----
3.13.0 (2025-10-06)
===================
Features
--------
- Added support for Python 3.14.
*Related issues and pull requests on GitHub:*
:issue:`10851`, :issue:`10872`.
- Added support for free-threading in Python 3.14+ -- by :user:`kumaraditya303`.
*Related issues and pull requests on GitHub:*
:issue:`11466`, :issue:`11464`.
- Added support for Zstandard (aka Zstd) compression
-- by :user:`KGuillaume-chaps`.
*Related issues and pull requests on GitHub:*
:issue:`11161`.
- Added ``StreamReader.total_raw_bytes`` to check the number of bytes downloaded
-- by :user:`robpats`.
*Related issues and pull requests on GitHub:*
:issue:`11483`.
Bug fixes
---------
- Fixed pytest plugin to not use deprecated :py:mod:`asyncio` policy APIs.
*Related issues and pull requests on GitHub:*
:issue:`10851`.
- Updated `Content-Disposition` header parsing to handle trailing semicolons and empty parts
-- by :user:`PLPeeters`.
*Related issues and pull requests on GitHub:*
:issue:`11243`.
- Fixed saved ``CookieJar`` failing to be loaded if cookies have ``partitioned`` flag when
``http.cookie`` does not have partitioned cookies supports. -- by :user:`Cycloctane`.
*Related issues and pull requests on GitHub:*
:issue:`11523`.
Improved documentation
----------------------
- Added ``Wireup`` to third-party libraries -- by :user:`maldoinc`.
*Related issues and pull requests on GitHub:*
:issue:`11233`.
Packaging updates and notes for downstreams
-------------------------------------------
- The `blockbuster` test dependency is now optional; the corresponding test fixture is disabled when it is unavailable
-- by :user:`musicinybrain`.
*Related issues and pull requests on GitHub:*
:issue:`11363`.
- Added ``riscv64`` build to releases -- by :user:`eshattow`.
*Related issues and pull requests on GitHub:*
:issue:`11425`.
Contributor-facing changes
--------------------------
- Fixed ``test_send_compress_text`` failing when alternative zlib implementation
is used. (``zlib-ng`` in python 3.14 windows build) -- by :user:`Cycloctane`.
*Related issues and pull requests on GitHub:*
:issue:`11546`.
----
3.12.15 (2025-07-28)
====================
Bug fixes
---------
- Fixed :class:`~aiohttp.DigestAuthMiddleware` to preserve the algorithm case from the server's challenge in the authorization response. This improves compatibility with servers that perform case-sensitive algorithm matching (e.g., servers expecting ``algorithm=MD5-sess`` instead of ``algorithm=MD5-SESS``)
-- by :user:`bdraco`.
*Related issues and pull requests on GitHub:*
:issue:`11352`.
Improved documentation
----------------------
- Remove outdated contents of ``aiohttp-devtools`` and ``aiohttp-swagger``
from Web_advanced docs.
-- by :user:`Cycloctane`
*Related issues and pull requests on GitHub:*
:issue:`11347`.
Packaging updates and notes for downstreams
-------------------------------------------
- Started including the ``llhttp`` :file:`LICENSE` file in wheels by adding ``vendor/llhttp/LICENSE`` to ``license-files`` in :file:`setup.cfg` -- by :user:`threexc`.
*Related issues and pull requests on GitHub:*
:issue:`11226`.
Contributor-facing changes
--------------------------
- Updated a regex in `test_aiohttp_request_coroutine` for Python 3.14.
*Related issues and pull requests on GitHub:*
:issue:`11271`.
----
3.12.14 (2025-07-10)
====================
Bug fixes
---------
- Fixed file uploads failing with HTTP 422 errors when encountering 307/308 redirects, and 301/302 redirects for non-POST methods, by preserving the request body when appropriate per :rfc:`9110#section-15.4.3-3.1` -- by :user:`bdraco`.
*Related issues and pull requests on GitHub:*
:issue:`11270`.
- Fixed :py:meth:`ClientSession.close() <aiohttp.ClientSession.close>` hanging indefinitely when using HTTPS requests through HTTP proxies -- by :user:`bdraco`.
*Related issues and pull requests on GitHub:*
:issue:`11273`.
- Bumped minimum version of aiosignal to 1.4+ to resolve typing issues -- by :user:`Dreamsorcerer`.
*Related issues and pull requests on GitHub:*
:issue:`11280`.
Features
--------
- Added initial trailer parsing logic to Python HTTP parser -- by :user:`Dreamsorcerer`.
*Related issues and pull requests on GitHub:*
:issue:`11269`.
Improved documentation
----------------------
- Clarified exceptions raised by ``WebSocketResponse.send_frame`` et al.
-- by :user:`DoctorJohn`.
*Related issues and pull requests on GitHub:*
:issue:`11234`.
----
3.12.13 (2025-06-14)
====================
@@ -4309,7 +4758,7 @@ Bugfixes
`#5853 <https://github.com/aio-libs/aiohttp/issues/5853>`_
- Added ``params`` keyword argument to ``ClientSession.ws_connect``. -- :user:`hoh`.
`#5868 <https://github.com/aio-libs/aiohttp/issues/5868>`_
- Uses :py:class:`~asyncio.ThreadedChildWatcher` under POSIX to allow setting up test loop in non-main thread.
- Uses ``asyncio.ThreadedChildWatcher`` under POSIX to allow setting up test loop in non-main thread.
`#5877 <https://github.com/aio-libs/aiohttp/issues/5877>`_
- Fix the error in handling the return value of `getaddrinfo`.
`getaddrinfo` will return an `(int, bytes)` tuple, if CPython could not handle the address family.
+10
View File
@@ -57,6 +57,7 @@ Arthur Darcet
Austin Scola
Bai Haoran
Ben Bader
Ben Beasley
Ben Greiner
Ben Kallus
Ben Timby
@@ -142,6 +143,7 @@ Gennady Andreyev
Georges Dubus
Greg Holt
Gregory Haynes
Guillaume Leurquin
Gus Goulart
Gustavo Carneiro
Günther Jena
@@ -169,6 +171,7 @@ Ivan Lakovic
Ivan Larin
J. Nick Koston
Jacob Champion
Jacob Henner
Jaesung Lee
Jake Davis
Jakob Ackermann
@@ -211,9 +214,11 @@ Justin Foo
Justin Turner Arthur
Kay Zheng
Kevin Samuel
Kilian Guillaume
Kimmo Parviainen-Jalanko
Kirill Klenov
Kirill Malovitsa
Kirill Potapenko
Konstantin Shutkin
Konstantin Valetov
Krzysztof Blazewicz
@@ -238,6 +243,7 @@ Marco Paolini
Marcus Stojcevich
Mariano Anaya
Mariusz Masztalerczuk
Mark Larah
Marko Kohtala
Martijn Pieters
Martin Melka
@@ -259,6 +265,7 @@ Mikhail Burshteyn
Mikhail Kashkin
Mikhail Lukyanchenko
Mikhail Nacharov
Mingjie Zhao
Misha Behersky
Mitchell Ferree
Morgan Delahaye-Prat
@@ -276,6 +283,7 @@ Pahaz Blinov
Panagiotis Kolokotronis
Pankaj Pandey
Parag Jain
Patrick Lee
Pau Freixes
Paul Colomiets
Paul J. Dorn
@@ -305,6 +313,7 @@ Roman Postnov
Rong Zhang
Samir Akarioh
Samuel Colvin
Samuel Gaist
Sean Hunt
Sebastian Acuna
Sebastian Hanula
@@ -343,6 +352,7 @@ Tim Menninger
Tolga Tezel
Tomasz Trebski
Toshiaki Tanaka
Trevor Gamblin
Trinh Hoang Nhu
Tymofii Tsiapa
Vadim Suharnikov
+1 -2
View File
@@ -9,8 +9,7 @@ graft examples
graft tests
graft tools
graft requirements
recursive-include vendor *
global-include aiohttp *.pyi
graft vendor
global-exclude *.pyc
global-exclude *.pyd
global-exclude *.so
+4 -4
View File
@@ -59,14 +59,14 @@ aiohttp/_find_header.c: $(call to-hash,aiohttp/hdrs.py ./tools/gen.py)
# Special case for reader since we want to be able to disable
# the extension with AIOHTTP_NO_EXTENSIONS
aiohttp/_websocket/reader_c.c: aiohttp/_websocket/reader_c.py
cython -3 -o $@ $< -I aiohttp -Werror
cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror
# _find_headers generator creates _headers.pyi as well
aiohttp/%.c: aiohttp/%.pyx $(call to-hash,$(CYS)) aiohttp/_find_header.c
cython -3 -o $@ $< -I aiohttp -Werror
cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror
aiohttp/_websocket/%.c: aiohttp/_websocket/%.pyx $(call to-hash,$(CYS))
cython -3 -o $@ $< -I aiohttp -Werror
cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror
vendor/llhttp/node_modules: vendor/llhttp/package.json
cd vendor/llhttp; npm ci
@@ -189,5 +189,5 @@ install-dev: .develop
.PHONY: sync-direct-runtime-deps
sync-direct-runtime-deps:
@echo Updating 'requirements/runtime-deps.in' from 'setup.cfg'... >&2
@echo Updating 'requirements/runtime-deps.in' from 'pyproject.toml'... >&2
@python requirements/sync-direct-runtime-deps.py
+31 -19
View File
@@ -1,11 +1,10 @@
Metadata-Version: 2.4
Name: aiohttp
Version: 3.12.13
Version: 3.13.3
Summary: Async http client/server framework (asyncio)
Home-page: https://github.com/aio-libs/aiohttp
Maintainer: aiohttp team <team@aiohttp.org>
Maintainer-email: team@aiohttp.org
License: Apache-2.0
Maintainer-email: aiohttp team <team@aiohttp.org>
License: Apache-2.0 AND MIT
Project-URL: Homepage, https://github.com/aio-libs/aiohttp
Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org
Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org
Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
@@ -27,12 +26,14 @@ Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.9
Description-Content-Type: text/x-rst
License-File: LICENSE.txt
License-File: vendor/llhttp/LICENSE
Requires-Dist: aiohappyeyeballs>=2.5.0
Requires-Dist: aiosignal>=1.1.2
Requires-Dist: aiosignal>=1.4.0
Requires-Dist: async-timeout<6.0,>=4.0; python_version < "3.11"
Requires-Dist: attrs>=17.3.0
Requires-Dist: frozenlist>=1.1.1
@@ -41,8 +42,9 @@ Requires-Dist: propcache>=0.2.0
Requires-Dist: yarl<2.0,>=1.17.0
Provides-Extra: speedups
Requires-Dist: aiodns>=3.3.0; extra == "speedups"
Requires-Dist: Brotli; platform_python_implementation == "CPython" and extra == "speedups"
Requires-Dist: brotlicffi; platform_python_implementation != "CPython" and extra == "speedups"
Requires-Dist: Brotli>=1.2; platform_python_implementation == "CPython" and extra == "speedups"
Requires-Dist: brotlicffi>=1.2; platform_python_implementation != "CPython" and extra == "speedups"
Requires-Dist: backports.zstd; (platform_python_implementation == "CPython" and python_version < "3.14") and extra == "speedups"
Dynamic: license-file
==================================
@@ -64,25 +66,21 @@ Async http client/server framework
:target: https://codecov.io/gh/aio-libs/aiohttp
:alt: codecov.io status for master branch
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
:target: https://codspeed.io/aio-libs/aiohttp
:alt: Codspeed.io status for aiohttp
.. image:: https://badge.fury.io/py/aiohttp.svg
:target: https://pypi.org/project/aiohttp
:alt: Latest PyPI package version
.. image:: https://img.shields.io/pypi/dm/aiohttp
:target: https://pypistats.org/packages/aiohttp
:alt: Downloads count
.. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest
:target: https://docs.aiohttp.org/
:alt: Latest Read The Docs
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs:matrix.org
:alt: Matrix Room — #aio-libs:matrix.org
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
:alt: Matrix Space — #aio-libs-space:matrix.org
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
:target: https://codspeed.io/aio-libs/aiohttp
:alt: Codspeed.io status for aiohttp
Key Features
@@ -248,3 +246,17 @@ Benchmarks
If you are interested in efficiency, the AsyncIO community maintains a
list of benchmarks on the official wiki:
https://github.com/python/asyncio/wiki/Benchmarks
--------
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs:matrix.org
:alt: Matrix Room — #aio-libs:matrix.org
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
:alt: Matrix Space — #aio-libs-space:matrix.org
.. image:: https://insights.linuxfoundation.org/api/badge/health-score?project=aiohttp
:target: https://insights.linuxfoundation.org/project/aiohttp
:alt: LFX Health Score
+21 -11
View File
@@ -17,25 +17,21 @@ Async http client/server framework
:target: https://codecov.io/gh/aio-libs/aiohttp
:alt: codecov.io status for master branch
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
:target: https://codspeed.io/aio-libs/aiohttp
:alt: Codspeed.io status for aiohttp
.. image:: https://badge.fury.io/py/aiohttp.svg
:target: https://pypi.org/project/aiohttp
:alt: Latest PyPI package version
.. image:: https://img.shields.io/pypi/dm/aiohttp
:target: https://pypistats.org/packages/aiohttp
:alt: Downloads count
.. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest
:target: https://docs.aiohttp.org/
:alt: Latest Read The Docs
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs:matrix.org
:alt: Matrix Room — #aio-libs:matrix.org
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
:alt: Matrix Space — #aio-libs-space:matrix.org
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
:target: https://codspeed.io/aio-libs/aiohttp
:alt: Codspeed.io status for aiohttp
Key Features
@@ -201,3 +197,17 @@ Benchmarks
If you are interested in efficiency, the AsyncIO community maintains a
list of benchmarks on the official wiki:
https://github.com/python/asyncio/wiki/Benchmarks
--------
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs:matrix.org
:alt: Matrix Room — #aio-libs:matrix.org
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
:alt: Matrix Space — #aio-libs-space:matrix.org
.. image:: https://insights.linuxfoundation.org/api/badge/health-score?project=aiohttp
:target: https://insights.linuxfoundation.org/project/aiohttp
:alt: LFX Health Score
+31 -19
View File
@@ -1,11 +1,10 @@
Metadata-Version: 2.4
Name: aiohttp
Version: 3.12.13
Version: 3.13.3
Summary: Async http client/server framework (asyncio)
Home-page: https://github.com/aio-libs/aiohttp
Maintainer: aiohttp team <team@aiohttp.org>
Maintainer-email: team@aiohttp.org
License: Apache-2.0
Maintainer-email: aiohttp team <team@aiohttp.org>
License: Apache-2.0 AND MIT
Project-URL: Homepage, https://github.com/aio-libs/aiohttp
Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org
Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org
Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
@@ -27,12 +26,14 @@ Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.9
Description-Content-Type: text/x-rst
License-File: LICENSE.txt
License-File: vendor/llhttp/LICENSE
Requires-Dist: aiohappyeyeballs>=2.5.0
Requires-Dist: aiosignal>=1.1.2
Requires-Dist: aiosignal>=1.4.0
Requires-Dist: async-timeout<6.0,>=4.0; python_version < "3.11"
Requires-Dist: attrs>=17.3.0
Requires-Dist: frozenlist>=1.1.1
@@ -41,8 +42,9 @@ Requires-Dist: propcache>=0.2.0
Requires-Dist: yarl<2.0,>=1.17.0
Provides-Extra: speedups
Requires-Dist: aiodns>=3.3.0; extra == "speedups"
Requires-Dist: Brotli; platform_python_implementation == "CPython" and extra == "speedups"
Requires-Dist: brotlicffi; platform_python_implementation != "CPython" and extra == "speedups"
Requires-Dist: Brotli>=1.2; platform_python_implementation == "CPython" and extra == "speedups"
Requires-Dist: brotlicffi>=1.2; platform_python_implementation != "CPython" and extra == "speedups"
Requires-Dist: backports.zstd; (platform_python_implementation == "CPython" and python_version < "3.14") and extra == "speedups"
Dynamic: license-file
==================================
@@ -64,25 +66,21 @@ Async http client/server framework
:target: https://codecov.io/gh/aio-libs/aiohttp
:alt: codecov.io status for master branch
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
:target: https://codspeed.io/aio-libs/aiohttp
:alt: Codspeed.io status for aiohttp
.. image:: https://badge.fury.io/py/aiohttp.svg
:target: https://pypi.org/project/aiohttp
:alt: Latest PyPI package version
.. image:: https://img.shields.io/pypi/dm/aiohttp
:target: https://pypistats.org/packages/aiohttp
:alt: Downloads count
.. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest
:target: https://docs.aiohttp.org/
:alt: Latest Read The Docs
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs:matrix.org
:alt: Matrix Room — #aio-libs:matrix.org
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
:alt: Matrix Space — #aio-libs-space:matrix.org
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
:target: https://codspeed.io/aio-libs/aiohttp
:alt: Codspeed.io status for aiohttp
Key Features
@@ -248,3 +246,17 @@ Benchmarks
If you are interested in efficiency, the AsyncIO community maintains a
list of benchmarks on the official wiki:
https://github.com/python/asyncio/wiki/Benchmarks
--------
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs:matrix.org
:alt: Matrix Room — #aio-libs:matrix.org
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
:alt: Matrix Space — #aio-libs-space:matrix.org
.. image:: https://insights.linuxfoundation.org/api/badge/health-score?project=aiohttp
:target: https://insights.linuxfoundation.org/project/aiohttp
:alt: LFX Health Score
+6 -1
View File
@@ -68,7 +68,6 @@ aiohttp/worker.py
aiohttp.egg-info/PKG-INFO
aiohttp.egg-info/SOURCES.txt
aiohttp.egg-info/dependency_links.txt
aiohttp.egg-info/not-zip-safe
aiohttp.egg-info/requires.txt
aiohttp.egg-info/top_level.txt
aiohttp/.hash/_cparser.pxd.hash
@@ -172,6 +171,8 @@ examples/web_srv_route_deco.py
examples/web_srv_route_table.py
examples/web_ws.py
examples/websocket.html
requirements/base-ft.in
requirements/base-ft.txt
requirements/base.in
requirements/base.txt
requirements/constraints.in
@@ -191,6 +192,10 @@ requirements/multidict.txt
requirements/runtime-deps.in
requirements/runtime-deps.txt
requirements/sync-direct-runtime-deps.py
requirements/test-common.in
requirements/test-common.txt
requirements/test-ft.in
requirements/test-ft.txt
requirements/test.in
requirements/test.txt
requirements/.hash/cython.txt.hash
@@ -1 +0,0 @@
+6 -3
View File
@@ -1,5 +1,5 @@
aiohappyeyeballs>=2.5.0
aiosignal>=1.1.2
aiosignal>=1.4.0
attrs>=17.3.0
frozenlist>=1.1.1
multidict<7.0,>=4.5
@@ -13,7 +13,10 @@ async-timeout<6.0,>=4.0
aiodns>=3.3.0
[speedups:platform_python_implementation != "CPython"]
brotlicffi
brotlicffi>=1.2
[speedups:platform_python_implementation == "CPython"]
Brotli
Brotli>=1.2
[speedups:platform_python_implementation == "CPython" and python_version < "3.14"]
backports.zstd
@@ -1 +1 @@
d4bd3b3cab898e00c642eaa59b2f7ae5ae5aa1374e698597f7d805a302f23e21 /home/runner/work/aiohttp/aiohttp/aiohttp/_http_parser.pyx
f9823c608638b8a77fec1c2bd28dc5c043f08c775ec59e7e262dd74b4ebb7384 /home/runner/work/aiohttp/aiohttp/aiohttp/_http_parser.pyx
@@ -1 +1 @@
f7ab1e2628277b82772d59c1dc3033c13495d769df67b1d1d49b1a474a75dd52 /home/runner/work/aiohttp/aiohttp/aiohttp/_http_writer.pyx
56514404ce87a15bfc6b400026d73a270165b2fdbe70313cfa007de29ddd7e14 /home/runner/work/aiohttp/aiohttp/aiohttp/_http_writer.pyx
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = "3.12.13"
__version__ = "3.13.3"
from typing import TYPE_CHECKING, Tuple
+40 -11
View File
@@ -6,7 +6,6 @@ These are not part of the public API and may change without notice.
"""
import re
import sys
from http.cookies import Morsel
from typing import List, Optional, Sequence, Tuple, cast
@@ -52,7 +51,7 @@ _COOKIE_PATTERN = re.compile(
\s* # Optional whitespace at start of cookie
(?P<key> # Start of group 'key'
# aiohttp has extended to include [] for compatibility with real-world cookies
[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=\[\]]+? # Any word of at least one letter
[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\[\]]+ # Any word of at least one letter
) # End of group 'key'
( # Optional group: there may not be a value.
\s*=\s* # Equal Sign
@@ -166,7 +165,10 @@ def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]:
attribute names (like 'path' or 'secure') should be treated as cookies.
This parser uses the same regex-based approach as parse_set_cookie_headers
to properly handle quoted values that may contain semicolons.
to properly handle quoted values that may contain semicolons. When the
regex fails to match a malformed cookie, it falls back to simple parsing
to ensure subsequent cookies are not lost
https://github.com/aio-libs/aiohttp/issues/11632
Args:
header: The Cookie header value to parse
@@ -178,14 +180,39 @@ def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]:
return []
cookies: List[Tuple[str, Morsel[str]]] = []
morsel: Morsel[str]
i = 0
n = len(header)
invalid_names = []
while i < n:
# Use the same pattern as parse_set_cookie_headers to find cookies
match = _COOKIE_PATTERN.match(header, i)
if not match:
break
# Fallback for malformed cookies https://github.com/aio-libs/aiohttp/issues/11632
# Find next semicolon to skip or attempt simple key=value parsing
next_semi = header.find(";", i)
eq_pos = header.find("=", i)
# Try to extract key=value if '=' comes before ';'
if eq_pos != -1 and (next_semi == -1 or eq_pos < next_semi):
end_pos = next_semi if next_semi != -1 else n
key = header[i:eq_pos].strip()
value = header[eq_pos + 1 : end_pos].strip()
# Validate the name (same as regex path)
if not _COOKIE_NAME_RE.match(key):
invalid_names.append(key)
else:
morsel = Morsel()
morsel.__setstate__( # type: ignore[attr-defined]
{"key": key, "value": _unquote(value), "coded_value": value}
)
cookies.append((key, morsel))
# Move to next cookie or end
i = next_semi + 1 if next_semi != -1 else n
continue
key = match.group("key")
value = match.group("val") or ""
@@ -193,11 +220,11 @@ def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]:
# Validate the name
if not key or not _COOKIE_NAME_RE.match(key):
internal_logger.warning("Can not load cookie: Illegal cookie name %r", key)
invalid_names.append(key)
continue
# Create new morsel
morsel: Morsel[str] = Morsel()
morsel = Morsel()
# Preserve the original value as coded_value (with quotes if present)
# We use __setstate__ instead of the public set() API because it allows us to
# bypass validation and set already validated state. This is more stable than
@@ -209,6 +236,11 @@ def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]:
cookies.append((key, morsel))
if invalid_names:
internal_logger.debug(
"Cannot load cookie. Illegal cookie names: %r", invalid_names
)
return cookies
@@ -270,11 +302,8 @@ def parse_set_cookie_headers(headers: Sequence[str]) -> List[Tuple[str, Morsel[s
break
if lower_key in _COOKIE_BOOL_ATTRS:
# Boolean attribute with any value should be True
if current_morsel is not None:
if lower_key == "partitioned" and sys.version_info < (3, 14):
dict.__setitem__(current_morsel, lower_key, True)
else:
current_morsel[lower_key] = True
if current_morsel is not None and current_morsel.isReservedKey(key):
current_morsel[lower_key] = True
elif value is None:
# Invalid cookie string - non-boolean attribute without value
break
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -1,5 +1,3 @@
#cython: language_level=3
#
# Based on https://github.com/MagicStack/httptools
#
@@ -421,7 +419,8 @@ cdef class HttpParser:
headers = CIMultiDictProxy(CIMultiDict(self._headers))
if self._cparser.type == cparser.HTTP_REQUEST:
allowed = upgrade and headers.get("upgrade", "").lower() in ALLOWED_UPGRADES
h_upg = headers.get("upgrade", "")
allowed = upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES
if allowed or self._cparser.method == cparser.HTTP_CONNECT:
self._upgraded = True
else:
@@ -436,8 +435,7 @@ cdef class HttpParser:
enc = self._content_encoding
if enc is not None:
self._content_encoding = None
enc = enc.lower()
if enc in ('gzip', 'deflate', 'br'):
if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}:
encoding = enc
if self._cparser.type == cparser.HTTP_REQUEST:
File diff suppressed because it is too large Load Diff
+9 -7
View File
@@ -8,7 +8,6 @@ from libc.string cimport memcpy
from multidict import istr
DEF BUF_SIZE = 16 * 1024 # 16KiB
cdef char BUFFER[BUF_SIZE]
cdef object _istr = istr
@@ -19,16 +18,17 @@ cdef struct Writer:
char *buf
Py_ssize_t size
Py_ssize_t pos
bint heap_allocated
cdef inline void _init_writer(Writer* writer):
writer.buf = &BUFFER[0]
cdef inline void _init_writer(Writer* writer, char *buf):
writer.buf = buf
writer.size = BUF_SIZE
writer.pos = 0
writer.heap_allocated = 0
cdef inline void _release_writer(Writer* writer):
if writer.buf != BUFFER:
if writer.heap_allocated:
PyMem_Free(writer.buf)
@@ -39,7 +39,7 @@ cdef inline int _write_byte(Writer* writer, uint8_t ch):
if writer.pos == writer.size:
# reallocate
size = writer.size + BUF_SIZE
if writer.buf == BUFFER:
if not writer.heap_allocated:
buf = <char*>PyMem_Malloc(size)
if buf == NULL:
PyErr_NoMemory()
@@ -52,6 +52,7 @@ cdef inline int _write_byte(Writer* writer, uint8_t ch):
return -1
writer.buf = buf
writer.size = size
writer.heap_allocated = 1
writer.buf[writer.pos] = <char>ch
writer.pos += 1
return 0
@@ -125,8 +126,9 @@ def _serialize_headers(str status_line, headers):
cdef Writer writer
cdef object key
cdef object val
cdef char buf[BUF_SIZE]
_init_writer(&writer)
_init_writer(&writer, buf)
try:
if _write_str(&writer, status_line) < 0:
+256 -68
View File
@@ -1,4 +1,4 @@
/* Generated by Cython 3.1.1 */
/* Generated by Cython 3.1.4 */
#ifndef PY_SSIZE_T_CLEAN
#define PY_SSIZE_T_CLEAN
@@ -14,8 +14,8 @@
#elif PY_VERSION_HEX < 0x03080000
#error Cython requires Python 3.8+.
#else
#define __PYX_ABI_VERSION "3_1_1"
#define CYTHON_HEX_VERSION 0x030101F0
#define __PYX_ABI_VERSION "3_1_4"
#define CYTHON_HEX_VERSION 0x030104F0
#define CYTHON_FUTURE_DIVISION 1
/* CModulePreamble */
#include <stddef.h>
@@ -378,6 +378,9 @@
enum { __pyx_check_sizeof_voidp = 1 / (int)(SIZEOF_VOID_P == sizeof(void*)) };
#endif
#endif
#ifndef CYTHON_LOCK_AND_GIL_DEADLOCK_AVOIDANCE_TIME
#define CYTHON_LOCK_AND_GIL_DEADLOCK_AVOIDANCE_TIME 100
#endif
#ifndef __has_attribute
#define __has_attribute(x) 0
#endif
@@ -430,7 +433,7 @@
#define CYTHON_MAYBE_UNUSED_VAR(x) CYTHON_UNUSED_VAR(x)
#endif
#ifndef CYTHON_NCP_UNUSED
# if CYTHON_COMPILING_IN_CPYTHON
# if CYTHON_COMPILING_IN_CPYTHON && !CYTHON_COMPILING_IN_CPYTHON_FREETHREADING
# define CYTHON_NCP_UNUSED
# else
# define CYTHON_NCP_UNUSED CYTHON_UNUSED
@@ -875,11 +878,19 @@ static CYTHON_INLINE PyObject * __Pyx_PyDict_GetItemStrWithError(PyObject *dict,
#define __Pyx_SET_REFCNT(obj, refcnt) Py_REFCNT(obj) = (refcnt)
#define __Pyx_SET_SIZE(obj, size) Py_SIZE(obj) = (size)
#endif
#if CYTHON_COMPILING_IN_LIMITED_API || CYTHON_AVOID_BORROWED_REFS || CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS || !CYTHON_ASSUME_SAFE_MACROS
#if CYTHON_AVOID_BORROWED_REFS || CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS
#if __PYX_LIMITED_VERSION_HEX >= 0x030d0000
#define __Pyx_PyList_GetItemRef(o, i) PyList_GetItemRef(o, i)
#elif CYTHON_COMPILING_IN_LIMITED_API || !CYTHON_ASSUME_SAFE_MACROS
#define __Pyx_PyList_GetItemRef(o, i) (likely((i) >= 0) ? PySequence_GetItem(o, i) : (PyErr_SetString(PyExc_IndexError, "list index out of range"), (PyObject*)NULL))
#else
#define __Pyx_PyList_GetItemRef(o, i) PySequence_ITEM(o, i)
#endif
#elif CYTHON_COMPILING_IN_LIMITED_API || !CYTHON_ASSUME_SAFE_MACROS
#if __PYX_LIMITED_VERSION_HEX >= 0x030d0000
#define __Pyx_PyList_GetItemRef(o, i) PyList_GetItemRef(o, i)
#else
#define __Pyx_PyList_GetItemRef(o, i) PySequence_GetItem(o, i)
#define __Pyx_PyList_GetItemRef(o, i) __Pyx_XNewRef(PyList_GetItem(o, i))
#endif
#else
#define __Pyx_PyList_GetItemRef(o, i) __Pyx_NewRef(PyList_GET_ITEM(o, i))
@@ -1331,7 +1342,9 @@ static CYTHON_INLINE Py_hash_t __Pyx_PyIndex_AsHash_t(PyObject*);
#endif /* __GNUC__ */
/* PretendToInitialize */
#ifdef __cplusplus
#if __cplusplus > 201103L
#include <type_traits>
#endif
template <typename T>
static void __Pyx_pretend_to_initialize(T* ptr) {
#if __cplusplus > 201103L
@@ -1897,24 +1910,33 @@ static void __Pyx_RaiseArgtupleInvalid(const char* func_name, int exact,
static int __Pyx__ArgTypeTest(PyObject *obj, PyTypeObject *type, const char *name, int exact);
/* TypeImport.proto */
#ifndef __PYX_HAVE_RT_ImportType_proto_3_1_1
#define __PYX_HAVE_RT_ImportType_proto_3_1_1
#ifndef __PYX_HAVE_RT_ImportType_proto_3_1_4
#define __PYX_HAVE_RT_ImportType_proto_3_1_4
#if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
#include <stdalign.h>
#endif
#if (defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L) || __cplusplus >= 201103L
#define __PYX_GET_STRUCT_ALIGNMENT_3_1_1(s) alignof(s)
#define __PYX_GET_STRUCT_ALIGNMENT_3_1_4(s) alignof(s)
#else
#define __PYX_GET_STRUCT_ALIGNMENT_3_1_1(s) sizeof(void*)
#define __PYX_GET_STRUCT_ALIGNMENT_3_1_4(s) sizeof(void*)
#endif
enum __Pyx_ImportType_CheckSize_3_1_1 {
__Pyx_ImportType_CheckSize_Error_3_1_1 = 0,
__Pyx_ImportType_CheckSize_Warn_3_1_1 = 1,
__Pyx_ImportType_CheckSize_Ignore_3_1_1 = 2
enum __Pyx_ImportType_CheckSize_3_1_4 {
__Pyx_ImportType_CheckSize_Error_3_1_4 = 0,
__Pyx_ImportType_CheckSize_Warn_3_1_4 = 1,
__Pyx_ImportType_CheckSize_Ignore_3_1_4 = 2
};
static PyTypeObject *__Pyx_ImportType_3_1_1(PyObject* module, const char *module_name, const char *class_name, size_t size, size_t alignment, enum __Pyx_ImportType_CheckSize_3_1_1 check_size);
static PyTypeObject *__Pyx_ImportType_3_1_4(PyObject* module, const char *module_name, const char *class_name, size_t size, size_t alignment, enum __Pyx_ImportType_CheckSize_3_1_4 check_size);
#endif
/* LimitedApiGetTypeDict.proto */
#if CYTHON_COMPILING_IN_LIMITED_API
static PyObject *__Pyx_GetTypeDict(PyTypeObject *tp);
#endif
/* SetItemOnTypeDict.proto */
static int __Pyx__SetItemOnTypeDict(PyTypeObject *tp, PyObject *k, PyObject *v);
#define __Pyx_SetItemOnTypeDict(tp, k, v) __Pyx__SetItemOnTypeDict((PyTypeObject*)tp, k, v)
/* FixUpExtensionType.proto */
static CYTHON_INLINE int __Pyx_fix_up_extension_type_from_spec(PyType_Spec *spec, PyTypeObject *type);
@@ -1925,7 +1947,11 @@ static PyObject *__Pyx_FetchSharedCythonABIModule(void);
static CYTHON_INLINE PyObject *__Pyx_PyDict_SetDefault(PyObject *d, PyObject *key, PyObject *default_value, int is_safe_type);
/* FetchCommonType.proto */
static PyTypeObject* __Pyx_FetchCommonTypeFromSpec(PyObject *module, PyType_Spec *spec, PyObject *bases);
static PyTypeObject* __Pyx_FetchCommonTypeFromSpec(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases);
/* CommonTypesMetaclass.proto */
static int __pyx_CommonTypesMetaclass_init(PyObject *module);
#define __Pyx_CommonTypesMetaclass_USED
/* CallTypeTraverse.proto */
#if !CYTHON_USE_TYPE_SPECS || (!CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX < 0x03090000)
@@ -2340,6 +2366,7 @@ static const char __pyx_k_range[] = "range";
static const char __pyx_k_module[] = "__module__";
static const char __pyx_k_add_note[] = "add_note";
static const char __pyx_k_qualname[] = "__qualname__";
static const char __pyx_k_set_name[] = "__set_name__";
static const char __pyx_k_is_coroutine[] = "_is_coroutine";
static const char __pyx_k_AssertionError[] = "AssertionError";
static const char __pyx_k_asyncio_coroutines[] = "asyncio.coroutines";
@@ -2347,7 +2374,7 @@ static const char __pyx_k_cline_in_traceback[] = "cline_in_traceback";
static const char __pyx_k_websocket_mask_cython[] = "_websocket_mask_cython";
static const char __pyx_k_aiohttp__websocket_mask[] = "aiohttp._websocket.mask";
static const char __pyx_k_aiohttp__websocket_mask_pyx[] = "aiohttp/_websocket/mask.pyx";
static const char __pyx_k_3avS_s_1_1_5Qa_Yaq_c_Q_k_D_is_q[] = "\200\001\360\026\000\005\014\2103\210a\210v\220S\230\001\340\004\017\210s\220!\2201\330\004\r\320\r\035\320\0351\260\021\260!\330\004\017\320\017%\320%5\260Q\260a\330\004\022\220+\230Y\240a\240q\360\n\000\005\010\200\220c\230\021\330\010\025\220Q\330\010\026\220k\240\023\240D\250\002\250!\340\010\016\210i\220s\230!\330\r\030\230\007\230q\240\006\240a\330\014\026\220a\330\014\030\230\001\360\006\000\005\013\210)\2203\220a\330\t\024\220G\2301\230F\240!\330\010\022\220!\330\010\024\220A\340\004\010\210\005\210U\220!\2203\220a\330\010\016\210a\210v\220X\230Q\230a";
static const char __pyx_k_3avS_s_1_1_5Qa_Yaq_c_Q_k_D_is_q[] = "\200\001\360\026\000\005\014\2103\210a\210v\220S\230\001\340\004\017\210s\220!\2201\330\004\r\320\r\035\320\0351\260\021\260!\330\004\017\320\017%\320%5\260Q\260a\330\004\022\220+\230Y\240a\240q\360\n\000\005\010\200\177\220c\230\021\330\010\025\220Q\330\010\026\220k\240\023\240D\250\002\250!\340\010\016\210i\220s\230!\330\r\030\230\007\230q\240\006\240a\330\014\026\220a\330\014\030\230\001\360\006\000\005\013\210)\2203\220a\330\t\024\220G\2301\230F\240!\330\010\022\220!\330\010\024\220A\340\004\010\210\005\210U\220!\2203\220a\330\010\016\210a\210v\220X\230Q\230a";
static const char __pyx_k_Note_that_Cython_is_deliberately[] = "Note that Cython is deliberately stricter than PEP-484 and rejects subclasses of builtin types. If you need to pass subclasses then set the 'annotation_typing' directive to False.";
/* #### Code section: decls ### */
static PyObject *__pyx_pf_7aiohttp_10_websocket_4mask__websocket_mask_cython(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_mask, PyObject *__pyx_v_data); /* proto */
@@ -2394,8 +2421,11 @@ typedef struct {
PyTypeObject *__pyx_ptype_7cpython_7complex_complex;
__Pyx_CachedCFunction __pyx_umethod_PyDict_Type_pop;
PyObject *__pyx_codeobj_tab[1];
PyObject *__pyx_string_tab[20];
PyObject *__pyx_string_tab[21];
/* #### Code section: module_state_contents ### */
/* CommonTypesMetaclass.module_state_decls */
PyTypeObject *__pyx_CommonTypesMetaclassType;
/* CachedMethodType.module_state_decls */
#if CYTHON_COMPILING_IN_LIMITED_API
PyObject *__Pyx_CachedMethodType;
@@ -2447,8 +2477,9 @@ static __pyx_mstatetype * const __pyx_mstate_global = &__pyx_mstate_global_stati
#define __pyx_n_u_pop __pyx_string_tab[15]
#define __pyx_n_u_qualname __pyx_string_tab[16]
#define __pyx_n_u_range __pyx_string_tab[17]
#define __pyx_n_u_test __pyx_string_tab[18]
#define __pyx_n_u_websocket_mask_cython __pyx_string_tab[19]
#define __pyx_n_u_set_name __pyx_string_tab[18]
#define __pyx_n_u_test __pyx_string_tab[19]
#define __pyx_n_u_websocket_mask_cython __pyx_string_tab[20]
/* #### Code section: module_state_clear ### */
#if CYTHON_USE_MODULE_STATE
static CYTHON_SMALL_CODE int __pyx_m_clear(PyObject *m) {
@@ -2473,7 +2504,7 @@ static CYTHON_SMALL_CODE int __pyx_m_clear(PyObject *m) {
Py_CLEAR(clear_module_state->__pyx_ptype_7cpython_4bool_bool);
Py_CLEAR(clear_module_state->__pyx_ptype_7cpython_7complex_complex);
for (int i=0; i<1; ++i) { Py_CLEAR(clear_module_state->__pyx_codeobj_tab[i]); }
for (int i=0; i<20; ++i) { Py_CLEAR(clear_module_state->__pyx_string_tab[i]); }
for (int i=0; i<21; ++i) { Py_CLEAR(clear_module_state->__pyx_string_tab[i]); }
return 0;
}
#endif
@@ -2498,7 +2529,7 @@ static CYTHON_SMALL_CODE int __pyx_m_traverse(PyObject *m, visitproc visit, void
Py_VISIT(traverse_module_state->__pyx_ptype_7cpython_4bool_bool);
Py_VISIT(traverse_module_state->__pyx_ptype_7cpython_7complex_complex);
for (int i=0; i<1; ++i) { __Pyx_VISIT_CONST(traverse_module_state->__pyx_codeobj_tab[i]); }
for (int i=0; i<20; ++i) { __Pyx_VISIT_CONST(traverse_module_state->__pyx_string_tab[i]); }
for (int i=0; i<21; ++i) { __Pyx_VISIT_CONST(traverse_module_state->__pyx_string_tab[i]); }
return 0;
}
#endif
@@ -3282,39 +3313,39 @@ static int __Pyx_modinit_type_import_code(__pyx_mstatetype *__pyx_mstate) {
/*--- Type import code ---*/
__pyx_t_1 = PyImport_ImportModule(__Pyx_BUILTIN_MODULE_NAME); if (unlikely(!__pyx_t_1)) __PYX_ERR(2, 9, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_1);
__pyx_mstate->__pyx_ptype_7cpython_4type_type = __Pyx_ImportType_3_1_1(__pyx_t_1, __Pyx_BUILTIN_MODULE_NAME, "type",
__pyx_mstate->__pyx_ptype_7cpython_4type_type = __Pyx_ImportType_3_1_4(__pyx_t_1, __Pyx_BUILTIN_MODULE_NAME, "type",
#if defined(PYPY_VERSION_NUM) && PYPY_VERSION_NUM < 0x050B0000
sizeof(PyTypeObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_1(PyTypeObject),
sizeof(PyTypeObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_4(PyTypeObject),
#elif CYTHON_COMPILING_IN_LIMITED_API
0, 0,
#else
sizeof(PyHeapTypeObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_1(PyHeapTypeObject),
sizeof(PyHeapTypeObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_4(PyHeapTypeObject),
#endif
__Pyx_ImportType_CheckSize_Warn_3_1_1); if (!__pyx_mstate->__pyx_ptype_7cpython_4type_type) __PYX_ERR(2, 9, __pyx_L1_error)
__Pyx_ImportType_CheckSize_Warn_3_1_4); if (!__pyx_mstate->__pyx_ptype_7cpython_4type_type) __PYX_ERR(2, 9, __pyx_L1_error)
__Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
__pyx_t_1 = PyImport_ImportModule(__Pyx_BUILTIN_MODULE_NAME); if (unlikely(!__pyx_t_1)) __PYX_ERR(3, 8, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_1);
__pyx_mstate->__pyx_ptype_7cpython_4bool_bool = __Pyx_ImportType_3_1_1(__pyx_t_1, __Pyx_BUILTIN_MODULE_NAME, "bool",
__pyx_mstate->__pyx_ptype_7cpython_4bool_bool = __Pyx_ImportType_3_1_4(__pyx_t_1, __Pyx_BUILTIN_MODULE_NAME, "bool",
#if defined(PYPY_VERSION_NUM) && PYPY_VERSION_NUM < 0x050B0000
sizeof(PyLongObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_1(PyLongObject),
sizeof(PyLongObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_4(PyLongObject),
#elif CYTHON_COMPILING_IN_LIMITED_API
0, 0,
#else
sizeof(PyLongObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_1(PyLongObject),
sizeof(PyLongObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_4(PyLongObject),
#endif
__Pyx_ImportType_CheckSize_Warn_3_1_1); if (!__pyx_mstate->__pyx_ptype_7cpython_4bool_bool) __PYX_ERR(3, 8, __pyx_L1_error)
__Pyx_ImportType_CheckSize_Warn_3_1_4); if (!__pyx_mstate->__pyx_ptype_7cpython_4bool_bool) __PYX_ERR(3, 8, __pyx_L1_error)
__Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
__pyx_t_1 = PyImport_ImportModule(__Pyx_BUILTIN_MODULE_NAME); if (unlikely(!__pyx_t_1)) __PYX_ERR(4, 16, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_1);
__pyx_mstate->__pyx_ptype_7cpython_7complex_complex = __Pyx_ImportType_3_1_1(__pyx_t_1, __Pyx_BUILTIN_MODULE_NAME, "complex",
__pyx_mstate->__pyx_ptype_7cpython_7complex_complex = __Pyx_ImportType_3_1_4(__pyx_t_1, __Pyx_BUILTIN_MODULE_NAME, "complex",
#if defined(PYPY_VERSION_NUM) && PYPY_VERSION_NUM < 0x050B0000
sizeof(PyComplexObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_1(PyComplexObject),
sizeof(PyComplexObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_4(PyComplexObject),
#elif CYTHON_COMPILING_IN_LIMITED_API
0, 0,
#else
sizeof(PyComplexObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_1(PyComplexObject),
sizeof(PyComplexObject), __PYX_GET_STRUCT_ALIGNMENT_3_1_4(PyComplexObject),
#endif
__Pyx_ImportType_CheckSize_Warn_3_1_1); if (!__pyx_mstate->__pyx_ptype_7cpython_7complex_complex) __PYX_ERR(4, 16, __pyx_L1_error)
__Pyx_ImportType_CheckSize_Warn_3_1_4); if (!__pyx_mstate->__pyx_ptype_7cpython_7complex_complex) __PYX_ERR(4, 16, __pyx_L1_error)
__Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
__Pyx_RefNannyFinishContext();
return 0;
@@ -3349,7 +3380,7 @@ static PyModuleDef_Slot __pyx_moduledef_slots[] = {
{Py_mod_create, (void*)__pyx_pymod_create},
{Py_mod_exec, (void*)__pyx_pymod_exec_mask},
#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING
{Py_mod_gil, Py_MOD_GIL_USED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
#if PY_VERSION_HEX >= 0x030C0000 && CYTHON_USE_MODULE_STATE
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
@@ -3549,7 +3580,7 @@ static CYTHON_SMALL_CODE int __pyx_pymod_exec_mask(PyObject *__pyx_pyinit_module
__pyx_m = __pyx_t_1;
#endif
#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING
PyUnstable_Module_SetGIL(__pyx_m, Py_MOD_GIL_USED);
PyUnstable_Module_SetGIL(__pyx_m, Py_MOD_GIL_NOT_USED);
#endif
__pyx_mstate = __pyx_mstate_global;
CYTHON_UNUSED_VAR(__pyx_t_1);
@@ -3577,6 +3608,13 @@ __Pyx_RefNannySetupContext("PyInit_mask", 0);
__pyx_mstate->__pyx_empty_tuple = PyTuple_New(0); if (unlikely(!__pyx_mstate->__pyx_empty_tuple)) __PYX_ERR(0, 1, __pyx_L1_error)
__pyx_mstate->__pyx_empty_bytes = PyBytes_FromStringAndSize("", 0); if (unlikely(!__pyx_mstate->__pyx_empty_bytes)) __PYX_ERR(0, 1, __pyx_L1_error)
__pyx_mstate->__pyx_empty_unicode = PyUnicode_FromStringAndSize("", 0); if (unlikely(!__pyx_mstate->__pyx_empty_unicode)) __PYX_ERR(0, 1, __pyx_L1_error)
/*--- Initialize various global constants etc. ---*/
if (__Pyx_InitConstants(__pyx_mstate) < 0) __PYX_ERR(0, 1, __pyx_L1_error)
stringtab_initialized = 1;
if (__Pyx_InitGlobals() < 0) __PYX_ERR(0, 1, __pyx_L1_error)
#if 0 || defined(__Pyx_CyFunction_USED) || defined(__Pyx_FusedFunction_USED) || defined(__Pyx_Coroutine_USED) || defined(__Pyx_Generator_USED) || defined(__Pyx_AsyncGen_USED)
if (__pyx_CommonTypesMetaclass_init(__pyx_m) < 0) __PYX_ERR(0, 1, __pyx_L1_error)
#endif
#ifdef __Pyx_CyFunction_USED
if (__pyx_CyFunction_init(__pyx_m) < 0) __PYX_ERR(0, 1, __pyx_L1_error)
#endif
@@ -3593,10 +3631,6 @@ __Pyx_RefNannySetupContext("PyInit_mask", 0);
if (__pyx_AsyncGen_init(__pyx_m) < 0) __PYX_ERR(0, 1, __pyx_L1_error)
#endif
/*--- Library function declarations ---*/
/*--- Initialize various global constants etc. ---*/
if (__Pyx_InitConstants(__pyx_mstate) < 0) __PYX_ERR(0, 1, __pyx_L1_error)
stringtab_initialized = 1;
if (__Pyx_InitGlobals() < 0) __PYX_ERR(0, 1, __pyx_L1_error)
if (__pyx_module_is_main_aiohttp___websocket__mask) {
if (PyObject_SetAttr(__pyx_m, __pyx_mstate_global->__pyx_n_u_name, __pyx_mstate_global->__pyx_n_u_main) < 0) __PYX_ERR(0, 1, __pyx_L1_error)
}
@@ -3719,6 +3753,7 @@ static const __Pyx_StringTabEntry __pyx_string_tab[] = {
{__pyx_k_pop, sizeof(__pyx_k_pop), 0, 1, 1}, /* PyObject cname: __pyx_n_u_pop */
{__pyx_k_qualname, sizeof(__pyx_k_qualname), 0, 1, 1}, /* PyObject cname: __pyx_n_u_qualname */
{__pyx_k_range, sizeof(__pyx_k_range), 0, 1, 1}, /* PyObject cname: __pyx_n_u_range */
{__pyx_k_set_name, sizeof(__pyx_k_set_name), 0, 1, 1}, /* PyObject cname: __pyx_n_u_set_name */
{__pyx_k_test, sizeof(__pyx_k_test), 0, 1, 1}, /* PyObject cname: __pyx_n_u_test */
{__pyx_k_websocket_mask_cython, sizeof(__pyx_k_websocket_mask_cython), 0, 1, 1}, /* PyObject cname: __pyx_n_u_websocket_mask_cython */
{0, 0, 0, 0, 0}
@@ -5195,15 +5230,15 @@ static int __Pyx__ArgTypeTest(PyObject *obj, PyTypeObject *type, const char *nam
}
/* TypeImport */
#ifndef __PYX_HAVE_RT_ImportType_3_1_1
#define __PYX_HAVE_RT_ImportType_3_1_1
static PyTypeObject *__Pyx_ImportType_3_1_1(PyObject *module, const char *module_name, const char *class_name,
size_t size, size_t alignment, enum __Pyx_ImportType_CheckSize_3_1_1 check_size)
#ifndef __PYX_HAVE_RT_ImportType_3_1_4
#define __PYX_HAVE_RT_ImportType_3_1_4
static PyTypeObject *__Pyx_ImportType_3_1_4(PyObject *module, const char *module_name, const char *class_name,
size_t size, size_t alignment, enum __Pyx_ImportType_CheckSize_3_1_4 check_size)
{
PyObject *result = 0;
Py_ssize_t basicsize;
Py_ssize_t itemsize;
#if CYTHON_COMPILING_IN_LIMITED_API
#ifdef Py_LIMITED_API
PyObject *py_basicsize;
PyObject *py_itemsize;
#endif
@@ -5216,7 +5251,7 @@ static PyTypeObject *__Pyx_ImportType_3_1_1(PyObject *module, const char *module
module_name, class_name);
goto bad;
}
#if !CYTHON_COMPILING_IN_LIMITED_API
#ifndef Py_LIMITED_API
basicsize = ((PyTypeObject *)result)->tp_basicsize;
itemsize = ((PyTypeObject *)result)->tp_itemsize;
#else
@@ -5254,7 +5289,7 @@ static PyTypeObject *__Pyx_ImportType_3_1_1(PyObject *module, const char *module
module_name, class_name, size, basicsize+itemsize);
goto bad;
}
if (check_size == __Pyx_ImportType_CheckSize_Error_3_1_1 &&
if (check_size == __Pyx_ImportType_CheckSize_Error_3_1_4 &&
((size_t)basicsize > size || (size_t)(basicsize + itemsize) < size)) {
PyErr_Format(PyExc_ValueError,
"%.200s.%.200s size changed, may indicate binary incompatibility. "
@@ -5262,7 +5297,7 @@ static PyTypeObject *__Pyx_ImportType_3_1_1(PyObject *module, const char *module
module_name, class_name, size, basicsize, basicsize+itemsize);
goto bad;
}
else if (check_size == __Pyx_ImportType_CheckSize_Warn_3_1_1 && (size_t)basicsize > size) {
else if (check_size == __Pyx_ImportType_CheckSize_Warn_3_1_4 && (size_t)basicsize > size) {
if (PyErr_WarnFormat(NULL, 0,
"%.200s.%.200s size changed, may indicate binary incompatibility. "
"Expected %zd from C header, got %zd from PyObject",
@@ -5277,24 +5312,83 @@ bad:
}
#endif
/* LimitedApiGetTypeDict */
#if CYTHON_COMPILING_IN_LIMITED_API
static Py_ssize_t __Pyx_GetTypeDictOffset(void) {
PyObject *tp_dictoffset_o;
Py_ssize_t tp_dictoffset;
tp_dictoffset_o = PyObject_GetAttrString((PyObject*)(&PyType_Type), "__dictoffset__");
if (unlikely(!tp_dictoffset_o)) return -1;
tp_dictoffset = PyLong_AsSsize_t(tp_dictoffset_o);
Py_DECREF(tp_dictoffset_o);
if (unlikely(tp_dictoffset == 0)) {
PyErr_SetString(
PyExc_TypeError,
"'type' doesn't have a dictoffset");
return -1;
} else if (unlikely(tp_dictoffset < 0)) {
PyErr_SetString(
PyExc_TypeError,
"'type' has an unexpected negative dictoffset. "
"Please report this as Cython bug");
return -1;
}
return tp_dictoffset;
}
static PyObject *__Pyx_GetTypeDict(PyTypeObject *tp) {
static Py_ssize_t tp_dictoffset = 0;
if (unlikely(tp_dictoffset == 0)) {
tp_dictoffset = __Pyx_GetTypeDictOffset();
if (unlikely(tp_dictoffset == -1 && PyErr_Occurred())) {
tp_dictoffset = 0; // try again next time?
return NULL;
}
}
return *(PyObject**)((char*)tp + tp_dictoffset);
}
#endif
/* SetItemOnTypeDict */
static int __Pyx__SetItemOnTypeDict(PyTypeObject *tp, PyObject *k, PyObject *v) {
int result;
PyObject *tp_dict;
#if CYTHON_COMPILING_IN_LIMITED_API
tp_dict = __Pyx_GetTypeDict(tp);
if (unlikely(!tp_dict)) return -1;
#else
tp_dict = tp->tp_dict;
#endif
result = PyDict_SetItem(tp_dict, k, v);
if (likely(!result)) {
PyType_Modified(tp);
if (unlikely(PyObject_HasAttr(v, __pyx_mstate_global->__pyx_n_u_set_name))) {
PyObject *setNameResult = PyObject_CallMethodObjArgs(v, __pyx_mstate_global->__pyx_n_u_set_name, (PyObject *) tp, k, NULL);
if (!setNameResult) return -1;
Py_DECREF(setNameResult);
}
}
return result;
}
/* FixUpExtensionType */
static int __Pyx_fix_up_extension_type_from_spec(PyType_Spec *spec, PyTypeObject *type) {
#if PY_VERSION_HEX > 0x030900B1 || CYTHON_COMPILING_IN_LIMITED_API
#if __PYX_LIMITED_VERSION_HEX > 0x030900B1
CYTHON_UNUSED_VAR(spec);
CYTHON_UNUSED_VAR(type);
CYTHON_UNUSED_VAR(__Pyx__SetItemOnTypeDict);
#else
const PyType_Slot *slot = spec->slots;
int changed = 0;
#if !CYTHON_COMPILING_IN_LIMITED_API
while (slot && slot->slot && slot->slot != Py_tp_members)
slot++;
if (slot && slot->slot == Py_tp_members) {
int changed = 0;
#if !(PY_VERSION_HEX <= 0x030900b1 && CYTHON_COMPILING_IN_CPYTHON)
#if !CYTHON_COMPILING_IN_CPYTHON
const
#endif
#endif // !CYTHON_COMPILING_IN_CPYTHON)
PyMemberDef *memb = (PyMemberDef*) slot->pfunc;
while (memb && memb->name) {
if (memb->name[0] == '_' && memb->name[1] == '_') {
#if PY_VERSION_HEX < 0x030900b1
if (strcmp(memb->name, "__weaklistoffset__") == 0) {
assert(memb->type == T_PYSSIZET);
assert(memb->flags == READONLY);
@@ -5318,11 +5412,8 @@ static int __Pyx_fix_up_extension_type_from_spec(PyType_Spec *spec, PyTypeObject
#endif
changed = 1;
}
#endif
#else
if ((0));
#endif
#if PY_VERSION_HEX <= 0x030900b1 && CYTHON_COMPILING_IN_CPYTHON
#endif // CYTHON_METH_FASTCALL
#if !CYTHON_COMPILING_IN_PYPY
else if (strcmp(memb->name, "__module__") == 0) {
PyObject *descr;
assert(memb->type == T_OBJECT);
@@ -5330,21 +5421,55 @@ static int __Pyx_fix_up_extension_type_from_spec(PyType_Spec *spec, PyTypeObject
descr = PyDescr_NewMember(type, memb);
if (unlikely(!descr))
return -1;
if (unlikely(PyDict_SetItem(type->tp_dict, PyDescr_NAME(descr), descr) < 0)) {
Py_DECREF(descr);
int set_item_result = PyDict_SetItem(type->tp_dict, PyDescr_NAME(descr), descr);
Py_DECREF(descr);
if (unlikely(set_item_result < 0)) {
return -1;
}
Py_DECREF(descr);
changed = 1;
}
#endif
#endif // !CYTHON_COMPILING_IN_PYPY
}
memb++;
}
if (changed)
PyType_Modified(type);
}
#endif
#endif // !CYTHON_COMPILING_IN_LIMITED_API
#if !CYTHON_COMPILING_IN_PYPY
slot = spec->slots;
while (slot && slot->slot && slot->slot != Py_tp_getset)
slot++;
if (slot && slot->slot == Py_tp_getset) {
PyGetSetDef *getset = (PyGetSetDef*) slot->pfunc;
while (getset && getset->name) {
if (getset->name[0] == '_' && getset->name[1] == '_' && strcmp(getset->name, "__module__") == 0) {
PyObject *descr = PyDescr_NewGetSet(type, getset);
if (unlikely(!descr))
return -1;
#if CYTHON_COMPILING_IN_LIMITED_API
PyObject *pyname = PyUnicode_FromString(getset->name);
if (unlikely(!pyname)) {
Py_DECREF(descr);
return -1;
}
int set_item_result = __Pyx_SetItemOnTypeDict(type, pyname, descr);
Py_DECREF(pyname);
#else
CYTHON_UNUSED_VAR(__Pyx__SetItemOnTypeDict);
int set_item_result = PyDict_SetItem(type->tp_dict, PyDescr_NAME(descr), descr);
#endif
Py_DECREF(descr);
if (unlikely(set_item_result < 0)) {
return -1;
}
changed = 1;
}
++getset;
}
}
#endif // !CYTHON_COMPILING_IN_PYPY
if (changed)
PyType_Modified(type);
#endif // PY_VERSION_HEX > 0x030900B1
return 0;
}
@@ -5371,6 +5496,24 @@ static CYTHON_INLINE PyObject *__Pyx_PyDict_SetDefault(PyObject *d, PyObject *ke
}
/* FetchCommonType */
#if __PYX_LIMITED_VERSION_HEX < 0x030C0000
static PyObject* __Pyx_PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases) {
PyObject *result = __Pyx_PyType_FromModuleAndSpec(module, spec, bases);
if (result && metaclass) {
PyObject *old_tp = (PyObject*)Py_TYPE(result);
Py_INCREF((PyObject*)metaclass);
#if __PYX_LIMITED_VERSION_HEX >= 0x03090000
Py_SET_TYPE(result, metaclass);
#else
result->ob_type = metaclass;
#endif
Py_DECREF(old_tp);
}
return result;
}
#else
#define __Pyx_PyType_FromMetaclass(me, mo, s, b) PyType_FromMetaclass(me, mo, s, b)
#endif
static int __Pyx_VerifyCachedType(PyObject *cached_type,
const char *name,
Py_ssize_t expected_basicsize) {
@@ -5380,6 +5523,9 @@ static int __Pyx_VerifyCachedType(PyObject *cached_type,
"Shared Cython type %.200s is not a type object", name);
return -1;
}
if (expected_basicsize == 0) {
return 0; // size is inherited, nothing useful to check
}
#if CYTHON_COMPILING_IN_LIMITED_API
PyObject *py_basicsize;
py_basicsize = PyObject_GetAttrString(cached_type, "__basicsize__");
@@ -5399,7 +5545,7 @@ static int __Pyx_VerifyCachedType(PyObject *cached_type,
}
return 0;
}
static PyTypeObject *__Pyx_FetchCommonTypeFromSpec(PyObject *module, PyType_Spec *spec, PyObject *bases) {
static PyTypeObject *__Pyx_FetchCommonTypeFromSpec(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases) {
PyObject *abi_module = NULL, *cached_type = NULL, *abi_module_dict, *new_cached_type, *py_object_name;
int get_item_ref_result;
const char* object_name = strrchr(spec->name, '.');
@@ -5423,7 +5569,7 @@ static PyTypeObject *__Pyx_FetchCommonTypeFromSpec(PyObject *module, PyType_Spec
goto bad;
}
CYTHON_UNUSED_VAR(module);
cached_type = __Pyx_PyType_FromModuleAndSpec(abi_module, spec, bases);
cached_type = __Pyx_PyType_FromMetaclass(metaclass, abi_module, spec, bases);
if (unlikely(!cached_type)) goto bad;
if (unlikely(__Pyx_fix_up_extension_type_from_spec(spec, (PyTypeObject *) cached_type) < 0)) goto bad;
new_cached_type = __Pyx_PyDict_SetDefault(abi_module_dict, py_object_name, cached_type, 1);
@@ -5452,6 +5598,43 @@ bad:
goto done;
}
/* CommonTypesMetaclass */
static PyObject* __pyx_CommonTypesMetaclass_get_module(CYTHON_UNUSED PyObject *self, CYTHON_UNUSED void* context) {
return PyUnicode_FromString(__PYX_ABI_MODULE_NAME);
}
static PyGetSetDef __pyx_CommonTypesMetaclass_getset[] = {
{"__module__", __pyx_CommonTypesMetaclass_get_module, NULL, NULL, NULL},
{0, 0, 0, 0, 0}
};
static PyType_Slot __pyx_CommonTypesMetaclass_slots[] = {
{Py_tp_getset, (void *)__pyx_CommonTypesMetaclass_getset},
{0, 0}
};
static PyType_Spec __pyx_CommonTypesMetaclass_spec = {
__PYX_TYPE_MODULE_PREFIX "_common_types_metatype",
0,
0,
#if PY_VERSION_HEX >= 0x030A0000
Py_TPFLAGS_IMMUTABLETYPE |
Py_TPFLAGS_DISALLOW_INSTANTIATION |
#endif
Py_TPFLAGS_DEFAULT,
__pyx_CommonTypesMetaclass_slots
};
static int __pyx_CommonTypesMetaclass_init(PyObject *module) {
__pyx_mstatetype *mstate = __Pyx_PyModule_GetState(module);
PyObject *bases = PyTuple_Pack(1, &PyType_Type);
if (unlikely(!bases)) {
return -1;
}
mstate->__pyx_CommonTypesMetaclassType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CommonTypesMetaclass_spec, bases);
Py_DECREF(bases);
if (unlikely(mstate->__pyx_CommonTypesMetaclassType == NULL)) {
return -1;
}
return 0;
}
/* CallTypeTraverse */
#if !CYTHON_USE_TYPE_SPECS || (!CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX < 0x03090000)
#else
@@ -6568,7 +6751,8 @@ static PyType_Spec __pyx_CyFunctionType_spec = {
};
static int __pyx_CyFunction_init(PyObject *module) {
__pyx_mstatetype *mstate = __Pyx_PyModule_GetState(module);
mstate->__pyx_CyFunctionType = __Pyx_FetchCommonTypeFromSpec(module, &__pyx_CyFunctionType_spec, NULL);
mstate->__pyx_CyFunctionType = __Pyx_FetchCommonTypeFromSpec(
mstate->__pyx_CommonTypesMetaclassType, module, &__pyx_CyFunctionType_spec, NULL);
if (unlikely(mstate->__pyx_CyFunctionType == NULL)) {
return -1;
}
@@ -7843,6 +8027,10 @@ bad:
PyCode_NewWithPosOnlyArgs
#endif
(a, p, k, l, s, f, code, c, n, v, fv, cell, fn, name, name, fline, lnos, __pyx_mstate_global->__pyx_empty_bytes);
#if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030c00A1
if (likely(result))
result->_co_firsttraceable = 0;
#endif
return result;
}
#elif PY_VERSION_HEX >= 0x030800B2 && !CYTHON_COMPILING_IN_PYPY
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -189,6 +189,13 @@ class WebSocketReader:
) -> None:
msg: WSMessage
if opcode in {OP_CODE_TEXT, OP_CODE_BINARY, OP_CODE_CONTINUATION}:
# Validate continuation frames before processing
if opcode == OP_CODE_CONTINUATION and self._opcode == OP_CODE_NOT_SET:
raise WebSocketError(
WSCloseCode.PROTOCOL_ERROR,
"Continuation frame for non started message",
)
# load text/binary
if not fin:
# got partial frame payload
@@ -205,11 +212,6 @@ class WebSocketReader:
has_partial = bool(self._partial)
if opcode == OP_CODE_CONTINUATION:
if self._opcode == OP_CODE_NOT_SET:
raise WebSocketError(
WSCloseCode.PROTOCOL_ERROR,
"Continuation frame for non started message",
)
opcode = self._opcode
self._opcode = OP_CODE_NOT_SET
# previous frame was non finished
+7 -5
View File
@@ -189,6 +189,13 @@ class WebSocketReader:
) -> None:
msg: WSMessage
if opcode in {OP_CODE_TEXT, OP_CODE_BINARY, OP_CODE_CONTINUATION}:
# Validate continuation frames before processing
if opcode == OP_CODE_CONTINUATION and self._opcode == OP_CODE_NOT_SET:
raise WebSocketError(
WSCloseCode.PROTOCOL_ERROR,
"Continuation frame for non started message",
)
# load text/binary
if not fin:
# got partial frame payload
@@ -205,11 +212,6 @@ class WebSocketReader:
has_partial = bool(self._partial)
if opcode == OP_CODE_CONTINUATION:
if self._opcode == OP_CODE_NOT_SET:
raise WebSocketError(
WSCloseCode.PROTOCOL_ERROR,
"Continuation frame for non started message",
)
opcode = self._opcode
self._opcode = OP_CODE_NOT_SET
# previous frame was non finished
+139 -55
View File
@@ -2,8 +2,9 @@
import asyncio
import random
import sys
from functools import partial
from typing import Any, Final, Optional, Union
from typing import Final, Optional, Set, Union
from ..base_protocol import BaseProtocol
from ..client_exceptions import ClientConnectionResetError
@@ -22,14 +23,18 @@ from .models import WS_DEFLATE_TRAILING, WSMsgType
DEFAULT_LIMIT: Final[int] = 2**16
# For websockets, keeping latency low is extremely important as implementations
# generally expect to be able to send and receive messages quickly. We use a
# larger chunk size than the default to reduce the number of executor calls
# since the executor is a significant source of latency and overhead when
# the chunks are small. A size of 5KiB was chosen because it is also the
# same value python-zlib-ng choose to use as the threshold to release the GIL.
# WebSocket opcode boundary: opcodes 0-7 are data frames, 8-15 are control frames
# Control frames (ping, pong, close) are never compressed
WS_CONTROL_FRAME_OPCODE: Final[int] = 8
WEBSOCKET_MAX_SYNC_CHUNK_SIZE = 5 * 1024
# For websockets, keeping latency low is extremely important as implementations
# generally expect to be able to send and receive messages quickly. We use a
# larger chunk size to reduce the number of executor calls and avoid task
# creation overhead, since both are significant sources of latency when chunks
# are small. A size of 16KiB was chosen as a balance between avoiding task
# overhead and not blocking the event loop too long with synchronous compression.
WEBSOCKET_MAX_SYNC_CHUNK_SIZE = 16 * 1024
class WebSocketWriter:
@@ -62,7 +67,9 @@ class WebSocketWriter:
self._closing = False
self._limit = limit
self._output_size = 0
self._compressobj: Any = None # actually compressobj
self._compressobj: Optional[ZLibCompressor] = None
self._send_lock = asyncio.Lock()
self._background_tasks: Set[asyncio.Task[None]] = set()
async def send_frame(
self, message: bytes, opcode: int, compress: Optional[int] = None
@@ -71,39 +78,57 @@ class WebSocketWriter:
if self._closing and not (opcode & WSMsgType.CLOSE):
raise ClientConnectionResetError("Cannot write to closing transport")
# RSV are the reserved bits in the frame header. They are used to
# indicate that the frame is using an extension.
# https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
rsv = 0
# Only compress larger packets (disabled)
# Does small packet needs to be compressed?
# if self.compress and opcode < 8 and len(message) > 124:
if (compress or self.compress) and opcode < 8:
# RSV1 (rsv = 0x40) is set for compressed frames
# https://datatracker.ietf.org/doc/html/rfc7692#section-7.2.3.1
rsv = 0x40
if not (compress or self.compress) or opcode >= WS_CONTROL_FRAME_OPCODE:
# Non-compressed frames don't need lock or shield
self._write_websocket_frame(message, opcode, 0)
elif len(message) <= WEBSOCKET_MAX_SYNC_CHUNK_SIZE:
# Small compressed payloads - compress synchronously in event loop
# We need the lock even though sync compression has no await points.
# This prevents small frames from interleaving with large frames that
# compress in the executor, avoiding compressor state corruption.
async with self._send_lock:
self._send_compressed_frame_sync(message, opcode, compress)
else:
# Large compressed frames need shield to prevent corruption
# For large compressed frames, the entire compress+send
# operation must be atomic. If cancelled after compression but
# before send, the compressor state would be advanced but data
# not sent, corrupting subsequent frames.
# Create a task to shield from cancellation
# The lock is acquired inside the shielded task so the entire
# operation (lock + compress + send) completes atomically.
# Use eager_start on Python 3.12+ to avoid scheduling overhead
loop = asyncio.get_running_loop()
coro = self._send_compressed_frame_async_locked(message, opcode, compress)
if sys.version_info >= (3, 12):
send_task = asyncio.Task(coro, loop=loop, eager_start=True)
else:
send_task = loop.create_task(coro)
# Keep a strong reference to prevent garbage collection
self._background_tasks.add(send_task)
send_task.add_done_callback(self._background_tasks.discard)
await asyncio.shield(send_task)
if compress:
# Do not set self._compress if compressing is for this frame
compressobj = self._make_compress_obj(compress)
else: # self.compress
if not self._compressobj:
self._compressobj = self._make_compress_obj(self.compress)
compressobj = self._compressobj
# It is safe to return control to the event loop when using compression
# after this point as we have already sent or buffered all the data.
# Once we have written output_size up to the limit, we call the
# drain helper which waits for the transport to be ready to accept
# more data. This is a flow control mechanism to prevent the buffer
# from growing too large. The drain helper will return right away
# if the writer is not paused.
if self._output_size > self._limit:
self._output_size = 0
if self.protocol._paused:
await self.protocol._drain_helper()
message = (
await compressobj.compress(message)
+ compressobj.flush(
ZLibBackend.Z_FULL_FLUSH
if self.notakeover
else ZLibBackend.Z_SYNC_FLUSH
)
).removesuffix(WS_DEFLATE_TRAILING)
# Its critical that we do not return control to the event
# loop until we have finished sending all the compressed
# data. Otherwise we could end up mixing compressed frames
# if there are multiple coroutines compressing data.
def _write_websocket_frame(self, message: bytes, opcode: int, rsv: int) -> None:
"""
Write a websocket frame to the transport.
This method handles frame header construction, masking, and writing to transport.
It does not handle compression or flow control - those are the responsibility
of the caller.
"""
msg_length = len(message)
use_mask = self.use_mask
@@ -146,26 +171,85 @@ class WebSocketWriter:
self._output_size += header_len + msg_length
# It is safe to return control to the event loop when using compression
# after this point as we have already sent or buffered all the data.
def _get_compressor(self, compress: Optional[int]) -> ZLibCompressor:
"""Get or create a compressor object for the given compression level."""
if compress:
# Do not set self._compress if compressing is for this frame
return ZLibCompressor(
level=ZLibBackend.Z_BEST_SPEED,
wbits=-compress,
max_sync_chunk_size=WEBSOCKET_MAX_SYNC_CHUNK_SIZE,
)
if not self._compressobj:
self._compressobj = ZLibCompressor(
level=ZLibBackend.Z_BEST_SPEED,
wbits=-self.compress,
max_sync_chunk_size=WEBSOCKET_MAX_SYNC_CHUNK_SIZE,
)
return self._compressobj
# Once we have written output_size up to the limit, we call the
# drain helper which waits for the transport to be ready to accept
# more data. This is a flow control mechanism to prevent the buffer
# from growing too large. The drain helper will return right away
# if the writer is not paused.
if self._output_size > self._limit:
self._output_size = 0
if self.protocol._paused:
await self.protocol._drain_helper()
def _send_compressed_frame_sync(
self, message: bytes, opcode: int, compress: Optional[int]
) -> None:
"""
Synchronous send for small compressed frames.
def _make_compress_obj(self, compress: int) -> ZLibCompressor:
return ZLibCompressor(
level=ZLibBackend.Z_BEST_SPEED,
wbits=-compress,
max_sync_chunk_size=WEBSOCKET_MAX_SYNC_CHUNK_SIZE,
This is used for small compressed payloads that compress synchronously in the event loop.
Since there are no await points, this is inherently cancellation-safe.
"""
# RSV are the reserved bits in the frame header. They are used to
# indicate that the frame is using an extension.
# https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
compressobj = self._get_compressor(compress)
# (0x40) RSV1 is set for compressed frames
# https://datatracker.ietf.org/doc/html/rfc7692#section-7.2.3.1
self._write_websocket_frame(
(
compressobj.compress_sync(message)
+ compressobj.flush(
ZLibBackend.Z_FULL_FLUSH
if self.notakeover
else ZLibBackend.Z_SYNC_FLUSH
)
).removesuffix(WS_DEFLATE_TRAILING),
opcode,
0x40,
)
async def _send_compressed_frame_async_locked(
self, message: bytes, opcode: int, compress: Optional[int]
) -> None:
"""
Async send for large compressed frames with lock.
Acquires the lock and compresses large payloads asynchronously in
the executor. The lock is held for the entire operation to ensure
the compressor state is not corrupted by concurrent sends.
MUST be run shielded from cancellation. If cancelled after
compression but before sending, the compressor state would be
advanced but data not sent, corrupting subsequent frames.
"""
async with self._send_lock:
# RSV are the reserved bits in the frame header. They are used to
# indicate that the frame is using an extension.
# https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
compressobj = self._get_compressor(compress)
# (0x40) RSV1 is set for compressed frames
# https://datatracker.ietf.org/doc/html/rfc7692#section-7.2.3.1
self._write_websocket_frame(
(
await compressobj.compress(message)
+ compressobj.flush(
ZLibBackend.Z_FULL_FLUSH
if self.notakeover
else ZLibBackend.Z_SYNC_FLUSH
)
).removesuffix(WS_DEFLATE_TRAILING),
opcode,
0x40,
)
async def close(self, code: int = 1000, message: Union[bytes, str] = b"") -> None:
"""Close the websocket, sending the specified code and message."""
if isinstance(message, str):
+1 -1
View File
@@ -122,7 +122,7 @@ class AbstractView(ABC):
return self._request
@abstractmethod
def __await__(self) -> Generator[Any, None, StreamResponse]:
def __await__(self) -> Generator[None, None, StreamResponse]:
"""Execute the view handler."""
+28
View File
@@ -98,7 +98,9 @@ from .helpers import (
EMPTY_BODY_METHODS,
BasicAuth,
TimeoutHandle,
basicauth_from_netrc,
get_env_proxy_for_url,
netrc_from_env,
sentinel,
strip_auth_from_url,
)
@@ -657,6 +659,13 @@ class ClientSession:
)
):
auth = self._default_auth
# Try netrc if auth is still None and trust_env is enabled.
if auth is None and self._trust_env and url.host is not None:
auth = await self._loop.run_in_executor(
None, self._get_netrc_auth, url.host
)
# It would be confusing if we support explicit
# Authorization header with auth argument
if (
@@ -821,6 +830,12 @@ class ClientSession:
data = None
if headers.get(hdrs.CONTENT_LENGTH):
headers.pop(hdrs.CONTENT_LENGTH)
else:
# For 307/308, always preserve the request body
# For 301/302 with non-POST methods, preserve the request body
# https://www.rfc-editor.org/rfc/rfc9110#section-15.4.3-3.1
# Use the existing payload to avoid recreating it from a potentially consumed file
data = req._body
r_url = resp.headers.get(hdrs.LOCATION) or resp.headers.get(
hdrs.URI
@@ -1205,6 +1220,19 @@ class ClientSession:
added_names.add(key)
return result
def _get_netrc_auth(self, host: str) -> Optional[BasicAuth]:
"""
Get auth from netrc for the given host.
This method is designed to be called in an executor to avoid
blocking I/O in the event loop.
"""
netrc_obj = netrc_from_env()
try:
return basicauth_from_netrc(netrc_obj, host)
except LookupError:
return None
if sys.version_info >= (3, 11) and TYPE_CHECKING:
def get(
@@ -10,6 +10,7 @@ variants, as well as both 'auth' and 'auth-int' quality of protection (qop) opti
import hashlib
import os
import re
import sys
import time
from typing import (
Callable,
@@ -60,24 +61,27 @@ DigestFunctions: Dict[str, Callable[[bytes], "hashlib._Hash"]] = {
# Compile the regex pattern once at module level for performance
_HEADER_PAIRS_PATTERN = re.compile(
r'(\w+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))'
# | | | | | | | | | || |
# +----|--|-|-|--|----|------|----|--||-----|--> alphanumeric key
# +--|-|-|--|----|------|----|--||-----|--> maybe whitespace
# | | | | | | | || |
# +-|-|--|----|------|----|--||-----|--> = (delimiter)
# +-|--|----|------|----|--||-----|--> maybe whitespace
# | | | | | || |
# +--|----|------|----|--||-----|--> group quoted or unquoted
# | | | | || |
# +----|------|----|--||-----|--> if quoted...
# +------|----|--||-----|--> anything but " or \
# +----|--||-----|--> escaped characters allowed
# +--||-----|--> or can be empty string
# || |
# +|-----|--> if unquoted...
# +-----|--> anything but , or <space>
# +--> at least one char req'd
r'(?:^|\s|,\s*)(\w+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))'
if sys.version_info < (3, 11)
else r'(?:^|\s|,\s*)((?>\w+))\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))'
# +------------|--------|--|-|-|--|----|------|----|--||-----|-> Match valid start/sep
# +--------|--|-|-|--|----|------|----|--||-----|-> alphanumeric key (atomic
# | | | | | | | | || | group reduces backtracking)
# +--|-|-|--|----|------|----|--||-----|-> maybe whitespace
# | | | | | | | || |
# +-|-|--|----|------|----|--||-----|-> = (delimiter)
# +-|--|----|------|----|--||-----|-> maybe whitespace
# | | | | | || |
# +--|----|------|----|--||-----|-> group quoted or unquoted
# | | | | || |
# +----|------|----|--||-----|-> if quoted...
# +------|----|--||-----|-> anything but " or \
# +----|--||-----|-> escaped characters allowed
# +--||-----|-> or can be empty string
# || |
# +|-----|-> if unquoted...
# +-----|-> anything but , or <space>
# +-> at least one char req'd
)
@@ -245,7 +249,9 @@ class DigestAuthMiddleware:
)
qop_raw = challenge.get("qop", "")
algorithm = challenge.get("algorithm", "MD5").upper()
# Preserve original algorithm case for response while using uppercase for processing
algorithm_original = challenge.get("algorithm", "MD5")
algorithm = algorithm_original.upper()
opaque = challenge.get("opaque", "")
# Convert string values to bytes once
@@ -342,7 +348,7 @@ class DigestAuthMiddleware:
"nonce": escape_quotes(nonce),
"uri": path,
"response": response_digest.decode(),
"algorithm": algorithm,
"algorithm": algorithm_original,
}
# Optional fields
+14 -11
View File
@@ -45,7 +45,7 @@ from .client_exceptions import (
InvalidURL,
ServerFingerprintMismatch,
)
from .compression_utils import HAS_BROTLI
from .compression_utils import HAS_BROTLI, HAS_ZSTD
from .formdata import FormData
from .helpers import (
_SENTINEL,
@@ -53,10 +53,9 @@ from .helpers import (
BasicAuth,
HeadersMixin,
TimerNoop,
basicauth_from_netrc,
netrc_from_env,
noop,
reify,
sentinel,
set_exception,
set_result,
)
@@ -104,7 +103,15 @@ json_re = re.compile(r"^application/(?:[\w.+-]+?\+)?json")
def _gen_default_accept_encoding() -> str:
return "gzip, deflate, br" if HAS_BROTLI else "gzip, deflate"
encodings = [
"gzip",
"deflate",
]
if HAS_BROTLI:
encodings.append("br")
if HAS_ZSTD:
encodings.append("zstd")
return ", ".join(encodings)
@attr.s(auto_attribs=True, frozen=True, slots=True)
@@ -128,14 +135,14 @@ class RequestInfo(_RequestInfo):
url: URL,
method: str,
headers: "CIMultiDictProxy[str]",
real_url: URL = _SENTINEL, # type: ignore[assignment]
real_url: Union[URL, _SENTINEL] = sentinel,
) -> "RequestInfo":
"""Create a new RequestInfo instance.
For backwards compatibility, the real_url parameter is optional.
"""
return tuple.__new__(
cls, (url, method, headers, url if real_url is _SENTINEL else real_url)
cls, (url, method, headers, url if real_url is sentinel else real_url)
)
@@ -1155,10 +1162,6 @@ class ClientRequest:
"""Set basic auth."""
if auth is None:
auth = self.auth
if auth is None and trust_env and self.url.host is not None:
netrc_obj = netrc_from_env()
with contextlib.suppress(LookupError):
auth = basicauth_from_netrc(netrc_obj, self.url.host)
if auth is None:
return
@@ -1326,7 +1329,7 @@ class ClientRequest:
self,
writer: AbstractStreamWriter,
conn: "Connection",
content_length: Optional[int],
content_length: Optional[int] = None,
) -> None:
"""
Write the request body to the connection stream.
+125 -55
View File
@@ -1,6 +1,7 @@
import asyncio
import sys
import zlib
from abc import ABC, abstractmethod
from concurrent.futures import Executor
from typing import Any, Final, Optional, Protocol, TypedDict, cast
@@ -21,7 +22,23 @@ try:
except ImportError: # pragma: no cover
HAS_BROTLI = False
MAX_SYNC_CHUNK_SIZE = 1024
try:
if sys.version_info >= (3, 14):
from compression.zstd import ZstdDecompressor # noqa: I900
else: # TODO(PY314): Remove mentions of backports.zstd across codebase
from backports.zstd import ZstdDecompressor
HAS_ZSTD = True
except ImportError:
HAS_ZSTD = False
MAX_SYNC_CHUNK_SIZE = 4096
DEFAULT_MAX_DECOMPRESS_SIZE = 2**25 # 32MiB
# Unlimited decompression constants - different libraries use different conventions
ZLIB_MAX_LENGTH_UNLIMITED = 0 # zlib uses 0 to mean unlimited
ZSTD_MAX_LENGTH_UNLIMITED = -1 # zstd uses -1 to mean unlimited
class ZLibCompressObjProtocol(Protocol):
@@ -133,19 +150,37 @@ def encoding_to_mode(
return -ZLibBackend.MAX_WBITS if suppress_deflate_header else ZLibBackend.MAX_WBITS
class ZlibBaseHandler:
class DecompressionBaseHandler(ABC):
def __init__(
self,
mode: int,
executor: Optional[Executor] = None,
max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE,
):
self._mode = mode
"""Base class for decompression handlers."""
self._executor = executor
self._max_sync_chunk_size = max_sync_chunk_size
@abstractmethod
def decompress_sync(
self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
) -> bytes:
"""Decompress the given data."""
class ZLibCompressor(ZlibBaseHandler):
async def decompress(
self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
) -> bytes:
"""Decompress the given data."""
if (
self._max_sync_chunk_size is not None
and len(data) > self._max_sync_chunk_size
):
return await asyncio.get_event_loop().run_in_executor(
self._executor, self.decompress_sync, data, max_length
)
return self.decompress_sync(data, max_length)
class ZLibCompressor:
def __init__(
self,
encoding: Optional[str] = None,
@@ -156,14 +191,12 @@ class ZLibCompressor(ZlibBaseHandler):
executor: Optional[Executor] = None,
max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE,
):
super().__init__(
mode=(
encoding_to_mode(encoding, suppress_deflate_header)
if wbits is None
else wbits
),
executor=executor,
max_sync_chunk_size=max_sync_chunk_size,
self._executor = executor
self._max_sync_chunk_size = max_sync_chunk_size
self._mode = (
encoding_to_mode(encoding, suppress_deflate_header)
if wbits is None
else wbits
)
self._zlib_backend: Final = ZLibBackendWrapper(ZLibBackend._zlib_backend)
@@ -174,7 +207,6 @@ class ZLibCompressor(ZlibBaseHandler):
if level is not None:
kwargs["level"] = level
self._compressor = self._zlib_backend.compressobj(**kwargs)
self._compress_lock = asyncio.Lock()
def compress_sync(self, data: bytes) -> bytes:
return self._compressor.compress(data)
@@ -187,28 +219,43 @@ class ZLibCompressor(ZlibBaseHandler):
If the data size is large than the max_sync_chunk_size, the compression
will be done in the executor. Otherwise, the compression will be done
in the event loop.
**WARNING: This method is NOT cancellation-safe when used with flush().**
If this operation is cancelled, the compressor state may be corrupted.
The connection MUST be closed after cancellation to avoid data corruption
in subsequent compress operations.
For cancellation-safe compression (e.g., WebSocket), the caller MUST wrap
compress() + flush() + send operations in a shield and lock to ensure atomicity.
"""
async with self._compress_lock:
# To ensure the stream is consistent in the event
# there are multiple writers, we need to lock
# the compressor so that only one writer can
# compress at a time.
if (
self._max_sync_chunk_size is not None
and len(data) > self._max_sync_chunk_size
):
return await asyncio.get_running_loop().run_in_executor(
self._executor, self._compressor.compress, data
)
return self.compress_sync(data)
# For large payloads, offload compression to executor to avoid blocking event loop
should_use_executor = (
self._max_sync_chunk_size is not None
and len(data) > self._max_sync_chunk_size
)
if should_use_executor:
return await asyncio.get_running_loop().run_in_executor(
self._executor, self._compressor.compress, data
)
return self.compress_sync(data)
def flush(self, mode: Optional[int] = None) -> bytes:
"""Flush the compressor synchronously.
**WARNING: This method is NOT cancellation-safe when called after compress().**
The flush() operation accesses shared compressor state. If compress() was
cancelled, calling flush() may result in corrupted data. The connection MUST
be closed after compress() cancellation.
For cancellation-safe compression (e.g., WebSocket), the caller MUST wrap
compress() + flush() + send operations in a shield and lock to ensure atomicity.
"""
return self._compressor.flush(
mode if mode is not None else self._zlib_backend.Z_FINISH
)
class ZLibDecompressor(ZlibBaseHandler):
class ZLibDecompressor(DecompressionBaseHandler):
def __init__(
self,
encoding: Optional[str] = None,
@@ -216,33 +263,16 @@ class ZLibDecompressor(ZlibBaseHandler):
executor: Optional[Executor] = None,
max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE,
):
super().__init__(
mode=encoding_to_mode(encoding, suppress_deflate_header),
executor=executor,
max_sync_chunk_size=max_sync_chunk_size,
)
super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size)
self._mode = encoding_to_mode(encoding, suppress_deflate_header)
self._zlib_backend: Final = ZLibBackendWrapper(ZLibBackend._zlib_backend)
self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode)
def decompress_sync(self, data: bytes, max_length: int = 0) -> bytes:
def decompress_sync(
self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
) -> bytes:
return self._decompressor.decompress(data, max_length)
async def decompress(self, data: bytes, max_length: int = 0) -> bytes:
"""Decompress the data and return the decompressed bytes.
If the data size is large than the max_sync_chunk_size, the decompression
will be done in the executor. Otherwise, the decompression will be done
in the event loop.
"""
if (
self._max_sync_chunk_size is not None
and len(data) > self._max_sync_chunk_size
):
return await asyncio.get_running_loop().run_in_executor(
self._executor, self._decompressor.decompress, data, max_length
)
return self.decompress_sync(data, max_length)
def flush(self, length: int = 0) -> bytes:
return (
self._decompressor.flush(length)
@@ -255,24 +285,64 @@ class ZLibDecompressor(ZlibBaseHandler):
return self._decompressor.eof
class BrotliDecompressor:
class BrotliDecompressor(DecompressionBaseHandler):
# Supports both 'brotlipy' and 'Brotli' packages
# since they share an import name. The top branches
# are for 'brotlipy' and bottom branches for 'Brotli'
def __init__(self) -> None:
def __init__(
self,
executor: Optional[Executor] = None,
max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE,
) -> None:
"""Decompress data using the Brotli library."""
if not HAS_BROTLI:
raise RuntimeError(
"The brotli decompression is not available. "
"Please install `Brotli` module"
)
self._obj = brotli.Decompressor()
super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size)
def decompress_sync(self, data: bytes) -> bytes:
def decompress_sync(
self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
) -> bytes:
"""Decompress the given data."""
if hasattr(self._obj, "decompress"):
return cast(bytes, self._obj.decompress(data))
return cast(bytes, self._obj.process(data))
return cast(bytes, self._obj.decompress(data, max_length))
return cast(bytes, self._obj.process(data, max_length))
def flush(self) -> bytes:
"""Flush the decompressor."""
if hasattr(self._obj, "flush"):
return cast(bytes, self._obj.flush())
return b""
class ZSTDDecompressor(DecompressionBaseHandler):
def __init__(
self,
executor: Optional[Executor] = None,
max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE,
) -> None:
if not HAS_ZSTD:
raise RuntimeError(
"The zstd decompression is not available. "
"Please install `backports.zstd` module"
)
self._obj = ZstdDecompressor()
super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size)
def decompress_sync(
self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
) -> bytes:
# zstd uses -1 for unlimited, while zlib uses 0 for unlimited
# Convert the zlib convention (0=unlimited) to zstd convention (-1=unlimited)
zstd_max_length = (
ZSTD_MAX_LENGTH_UNLIMITED
if max_length == ZLIB_MAX_LENGTH_UNLIMITED
else max_length
)
return self._obj.decompress(data, zstd_max_length)
def flush(self) -> bytes:
return b""
+52 -24
View File
@@ -229,6 +229,26 @@ class Connection:
return self._protocol is None or not self._protocol.is_connected()
class _ConnectTunnelConnection(Connection):
"""Special connection wrapper for CONNECT tunnels that must never be pooled.
This connection wraps the proxy connection that will be upgraded with TLS.
It must never be released to the pool because:
1. Its 'closed' future will never complete, causing session.close() to hang
2. It represents an intermediate state, not a reusable connection
3. The real connection (with TLS) will be created separately
"""
def release(self) -> None:
"""Do nothing - don't pool or close the connection.
These connections are an intermediate state during the CONNECT tunnel
setup and will be cleaned up naturally after the TLS upgrade. If they
were to be pooled, they would never be properly closed, causing
session.close() to wait forever for their 'closed' future.
"""
class _TransportPlaceholder:
"""placeholder for BaseConnector.connect function"""
@@ -589,6 +609,32 @@ class BaseConnector:
return total_remain
def _update_proxy_auth_header_and_build_proxy_req(
self, req: ClientRequest
) -> ClientRequest:
"""Set Proxy-Authorization header for non-SSL proxy requests and builds the proxy request for SSL proxy requests."""
url = req.proxy
assert url is not None
headers: Dict[str, str] = {}
if req.proxy_headers is not None:
headers = req.proxy_headers # type: ignore[assignment]
headers[hdrs.HOST] = req.headers[hdrs.HOST]
proxy_req = ClientRequest(
hdrs.METH_GET,
url,
headers=headers,
auth=req.proxy_auth,
loop=self._loop,
ssl=req.ssl,
)
auth = proxy_req.headers.pop(hdrs.AUTHORIZATION, None)
if auth is not None:
if not req.is_ssl():
req.headers[hdrs.PROXY_AUTHORIZATION] = auth
else:
proxy_req.headers[hdrs.PROXY_AUTHORIZATION] = auth
return proxy_req
async def connect(
self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
) -> Connection:
@@ -597,12 +643,16 @@ class BaseConnector:
if (conn := await self._get(key, traces)) is not None:
# If we do not have to wait and we can get a connection from the pool
# we can avoid the timeout ceil logic and directly return the connection
if req.proxy:
self._update_proxy_auth_header_and_build_proxy_req(req)
return conn
async with ceil_timeout(timeout.connect, timeout.ceil_threshold):
if self._available_connections(key) <= 0:
await self._wait_for_available_connection(key, traces)
if (conn := await self._get(key, traces)) is not None:
if req.proxy:
self._update_proxy_auth_header_and_build_proxy_req(req)
return conn
placeholder = cast(
@@ -1565,35 +1615,13 @@ class TCPConnector(BaseConnector):
) -> Tuple[asyncio.BaseTransport, ResponseHandler]:
self._fail_on_no_start_tls(req)
runtime_has_start_tls = self._loop_supports_start_tls()
headers: Dict[str, str] = {}
if req.proxy_headers is not None:
headers = req.proxy_headers # type: ignore[assignment]
headers[hdrs.HOST] = req.headers[hdrs.HOST]
url = req.proxy
assert url is not None
proxy_req = ClientRequest(
hdrs.METH_GET,
url,
headers=headers,
auth=req.proxy_auth,
loop=self._loop,
ssl=req.ssl,
)
proxy_req = self._update_proxy_auth_header_and_build_proxy_req(req)
# create connection to proxy server
transport, proto = await self._create_direct_connection(
proxy_req, [], timeout, client_error=ClientProxyConnectionError
)
auth = proxy_req.headers.pop(hdrs.AUTHORIZATION, None)
if auth is not None:
if not req.is_ssl():
req.headers[hdrs.PROXY_AUTHORIZATION] = auth
else:
proxy_req.headers[hdrs.PROXY_AUTHORIZATION] = auth
if req.is_ssl():
if runtime_has_start_tls:
self._warn_about_tls_in_tls(transport, req)
@@ -1612,7 +1640,7 @@ class TCPConnector(BaseConnector):
key = req.connection_key._replace(
proxy=None, proxy_auth=None, proxy_headers_hash=None
)
conn = Connection(self, key, proto, self._loop)
conn = _ConnectTunnelConnection(self, key, proto, self._loop)
proxy_resp = await proxy_req.send(conn)
try:
protocol = conn._protocol
+1 -1
View File
@@ -110,7 +110,7 @@ class FormData:
elif isinstance(rec, (list, tuple)) and len(rec) == 2:
k, fp = rec
self.add_field(k, fp) # type: ignore[arg-type]
self.add_field(k, fp)
else:
raise TypeError(
+30 -2
View File
@@ -17,7 +17,9 @@ import time
import weakref
from collections import namedtuple
from contextlib import suppress
from email.message import EmailMessage
from email.parser import HeaderParser
from email.policy import HTTP
from email.utils import parsedate
from math import ceil
from pathlib import Path
@@ -357,14 +359,40 @@ def parse_mimetype(mimetype: str) -> MimeType:
)
class EnsureOctetStream(EmailMessage):
def __init__(self) -> None:
super().__init__()
# https://www.rfc-editor.org/rfc/rfc9110#section-8.3-5
self.set_default_type("application/octet-stream")
def get_content_type(self) -> str:
"""Re-implementation from Message
Returns application/octet-stream in place of plain/text when
value is wrong.
The way this class is used guarantees that content-type will
be present so simplify the checks wrt to the base implementation.
"""
value = self.get("content-type", "").lower()
# Based on the implementation of _splitparam in the standard library
ctype, _, _ = value.partition(";")
ctype = ctype.strip()
if ctype.count("/") != 1:
return self.get_default_type()
return ctype
@functools.lru_cache(maxsize=56)
def parse_content_type(raw: str) -> Tuple[str, MappingProxyType[str, str]]:
"""Parse Content-Type header.
Returns a tuple of the parsed content type and a
MappingProxyType of parameters.
MappingProxyType of parameters. The default returned value
is `application/octet-stream`
"""
msg = HeaderParser().parsestr(f"Content-Type: {raw}")
msg = HeaderParser(EnsureOctetStream, policy=HTTP).parsestr(f"Content-Type: {raw}")
content_type = msg.get_content_type()
params = msg.get_params(())
content_dict = dict(params[1:]) # First element is content type again
+4
View File
@@ -74,6 +74,10 @@ class ContentLengthError(PayloadEncodingError):
"""Not enough data to satisfy content length header."""
class DecompressSizeError(PayloadEncodingError):
"""Decompressed size exceeds the configured limit."""
class LineTooLong(BadHttpMessage):
def __init__(
self, line: str, limit: str = "Unknown", actual_size: str = "Unknown"
+84 -44
View File
@@ -26,7 +26,14 @@ from yarl import URL
from . import hdrs
from .base_protocol import BaseProtocol
from .compression_utils import HAS_BROTLI, BrotliDecompressor, ZLibDecompressor
from .compression_utils import (
DEFAULT_MAX_DECOMPRESS_SIZE,
HAS_BROTLI,
HAS_ZSTD,
BrotliDecompressor,
ZLibDecompressor,
ZSTDDecompressor,
)
from .helpers import (
_EXC_SENTINEL,
DEBUG,
@@ -42,6 +49,7 @@ from .http_exceptions import (
BadStatusLine,
ContentEncodingError,
ContentLengthError,
DecompressSizeError,
InvalidHeader,
InvalidURLError,
LineTooLong,
@@ -142,8 +150,8 @@ class HeadersParser:
# note: "raw" does not mean inclusion of OWS before/after the field value
raw_headers = []
lines_idx = 1
line = lines[1]
lines_idx = 0
line = lines[lines_idx]
line_count = len(lines)
while line:
@@ -232,7 +240,9 @@ class HeadersParser:
def _is_supported_upgrade(headers: CIMultiDictProxy[str]) -> bool:
"""Check if the upgrade header is supported."""
return headers.get(hdrs.UPGRADE, "").lower() in {"tcp", "websocket"}
u = headers.get(hdrs.UPGRADE, "")
# .lower() can transform non-ascii characters.
return u.isascii() and u.lower() in {"tcp", "websocket"}
class HttpParser(abc.ABC, Generic[_MsgT]):
@@ -400,6 +410,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
response_with_body=self.response_with_body,
auto_decompress=self._auto_decompress,
lax=self.lax,
headers_parser=self._headers_parser,
)
if not payload_parser.done:
self._payload_parser = payload_parser
@@ -418,6 +429,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
compression=msg.compression,
auto_decompress=self._auto_decompress,
lax=self.lax,
headers_parser=self._headers_parser,
)
elif not empty_body and length is None and self.read_until_eof:
payload = StreamReader(
@@ -436,6 +448,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
response_with_body=self.response_with_body,
auto_decompress=self._auto_decompress,
lax=self.lax,
headers_parser=self._headers_parser,
)
if not payload_parser.done:
self._payload_parser = payload_parser
@@ -473,6 +486,10 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
eof = True
data = b""
if isinstance(
underlying_exc, (InvalidHeader, TransferEncodingError)
):
raise
if eof:
start_pos = 0
@@ -536,11 +553,9 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
upgrade = True
# encoding
enc = headers.get(hdrs.CONTENT_ENCODING)
if enc:
enc = enc.lower()
if enc in ("gzip", "deflate", "br"):
encoding = enc
enc = headers.get(hdrs.CONTENT_ENCODING, "")
if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}:
encoding = enc
# chunking
te = headers.get(hdrs.TRANSFER_ENCODING)
@@ -635,7 +650,7 @@ class HttpRequestParser(HttpParser[RawRequestMessage]):
compression,
upgrade,
chunked,
) = self.parse_headers(lines)
) = self.parse_headers(lines[1:])
if close is None: # then the headers weren't set in the request
if version_o <= HttpVersion10: # HTTP 1.0 must asks to not close
@@ -657,7 +672,9 @@ class HttpRequestParser(HttpParser[RawRequestMessage]):
)
def _is_chunked_te(self, te: str) -> bool:
if te.rsplit(",", maxsplit=1)[-1].strip(" \t").lower() == "chunked":
te = te.rsplit(",", maxsplit=1)[-1].strip(" \t")
# .lower() transforms some non-ascii chars, so must check first.
if te.isascii() and te.lower() == "chunked":
return True
# https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3
raise BadHttpMessage("Request has invalid `Transfer-Encoding`")
@@ -721,7 +738,7 @@ class HttpResponseParser(HttpParser[RawResponseMessage]):
compression,
upgrade,
chunked,
) = self.parse_headers(lines)
) = self.parse_headers(lines[1:])
if close is None:
if version_o <= HttpVersion10:
@@ -764,6 +781,8 @@ class HttpPayloadParser:
response_with_body: bool = True,
auto_decompress: bool = True,
lax: bool = False,
*,
headers_parser: HeadersParser,
) -> None:
self._length = 0
self._type = ParseState.PARSE_UNTIL_EOF
@@ -772,6 +791,8 @@ class HttpPayloadParser:
self._chunk_tail = b""
self._auto_decompress = auto_decompress
self._lax = lax
self._headers_parser = headers_parser
self._trailer_lines: list[bytes] = []
self.done = False
# payload decompression wrapper
@@ -848,7 +869,7 @@ class HttpPayloadParser:
size_b = chunk[:i] # strip chunk-extensions
# Verify no LF in the chunk-extension
if b"\n" in (ext := chunk[i:pos]):
exc = BadHttpMessage(
exc = TransferEncodingError(
f"Unexpected LF in chunk-extension: {ext!r}"
)
set_exception(self.payload, exc)
@@ -869,7 +890,7 @@ class HttpPayloadParser:
chunk = chunk[pos + len(SEP) :]
if size == 0: # eof marker
self._chunk = ChunkState.PARSE_MAYBE_TRAILERS
self._chunk = ChunkState.PARSE_TRAILERS
if self._lax and chunk.startswith(b"\r"):
chunk = chunk[1:]
else:
@@ -907,38 +928,31 @@ class HttpPayloadParser:
self._chunk_tail = chunk
return False, b""
# if stream does not contain trailer, after 0\r\n
# we should get another \r\n otherwise
# trailers needs to be skipped until \r\n\r\n
if self._chunk == ChunkState.PARSE_MAYBE_TRAILERS:
head = chunk[: len(SEP)]
if head == SEP:
# end of stream
self.payload.feed_eof()
return True, chunk[len(SEP) :]
# Both CR and LF, or only LF may not be received yet. It is
# expected that CRLF or LF will be shown at the very first
# byte next time, otherwise trailers should come. The last
# CRLF which marks the end of response might not be
# contained in the same TCP segment which delivered the
# size indicator.
if not head:
return False, b""
if head == SEP[:1]:
self._chunk_tail = head
return False, b""
self._chunk = ChunkState.PARSE_TRAILERS
# read and discard trailer up to the CRLF terminator
if self._chunk == ChunkState.PARSE_TRAILERS:
pos = chunk.find(SEP)
if pos >= 0:
chunk = chunk[pos + len(SEP) :]
self._chunk = ChunkState.PARSE_MAYBE_TRAILERS
else:
if pos < 0: # No line found
self._chunk_tail = chunk
return False, b""
line = chunk[:pos]
chunk = chunk[pos + len(SEP) :]
if SEP == b"\n": # For lax response parsing
line = line.rstrip(b"\r")
self._trailer_lines.append(line)
# \r\n\r\n found, end of stream
if self._trailer_lines[-1] == b"":
# Headers and trailers are defined the same way,
# so we reuse the HeadersParser here.
try:
trailers, raw_trailers = self._headers_parser.parse_headers(
self._trailer_lines
)
finally:
self._trailer_lines.clear()
self.payload.feed_eof()
return True, chunk
# Read all bytes until eof
elif self._type == ParseState.PARSE_UNTIL_EOF:
self.payload.feed_data(chunk, len(chunk))
@@ -951,13 +965,19 @@ class DeflateBuffer:
decompressor: Any
def __init__(self, out: StreamReader, encoding: Optional[str]) -> None:
def __init__(
self,
out: StreamReader,
encoding: Optional[str],
max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE,
) -> None:
self.out = out
self.size = 0
out.total_compressed_bytes = self.size
self.encoding = encoding
self._started_decoding = False
self.decompressor: Union[BrotliDecompressor, ZLibDecompressor]
self.decompressor: Union[BrotliDecompressor, ZLibDecompressor, ZSTDDecompressor]
if encoding == "br":
if not HAS_BROTLI: # pragma: no cover
raise ContentEncodingError(
@@ -965,9 +985,18 @@ class DeflateBuffer:
"Please install `Brotli`"
)
self.decompressor = BrotliDecompressor()
elif encoding == "zstd":
if not HAS_ZSTD:
raise ContentEncodingError(
"Can not decode content-encoding: zstandard (zstd). "
"Please install `backports.zstd`"
)
self.decompressor = ZSTDDecompressor()
else:
self.decompressor = ZLibDecompressor(encoding=encoding)
self._max_decompress_size = max_decompress_size
def set_exception(
self,
exc: BaseException,
@@ -980,6 +1009,7 @@ class DeflateBuffer:
return
self.size += size
self.out.total_compressed_bytes = self.size
# RFC1950
# bits 0..3 = CM = 0b1000 = 8 = "deflate"
@@ -996,7 +1026,10 @@ class DeflateBuffer:
)
try:
chunk = self.decompressor.decompress_sync(chunk)
# Decompress with limit + 1 so we can detect if output exceeds limit
chunk = self.decompressor.decompress_sync(
chunk, max_length=self._max_decompress_size + 1
)
except Exception:
raise ContentEncodingError(
"Can not decode content-encoding: %s" % self.encoding
@@ -1004,6 +1037,13 @@ class DeflateBuffer:
self._started_decoding = True
# Check if decompression limit was exceeded
if len(chunk) > self._max_decompress_size:
raise DecompressSizeError(
"Decompressed data exceeds the configured limit of %d bytes"
% self._max_decompress_size
)
if chunk:
self.out.feed_data(chunk, len(chunk))
+37 -25
View File
@@ -25,7 +25,12 @@ from urllib.parse import parse_qsl, unquote, urlencode
from multidict import CIMultiDict, CIMultiDictProxy
from .compression_utils import ZLibCompressor, ZLibDecompressor
from .abc import AbstractStreamWriter
from .compression_utils import (
DEFAULT_MAX_DECOMPRESS_SIZE,
ZLibCompressor,
ZLibDecompressor,
)
from .hdrs import (
CONTENT_DISPOSITION,
CONTENT_ENCODING,
@@ -114,6 +119,10 @@ def parse_content_disposition(
while parts:
item = parts.pop(0)
if not item: # To handle trailing semicolons
warnings.warn(BadContentDispositionHeader(header))
continue
if "=" not in item:
warnings.warn(BadContentDispositionHeader(header))
return None, {}
@@ -269,6 +278,7 @@ class BodyPartReader:
*,
subtype: str = "mixed",
default_charset: Optional[str] = None,
max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE,
) -> None:
self.headers = headers
self._boundary = boundary
@@ -285,6 +295,7 @@ class BodyPartReader:
self._prev_chunk: Optional[bytes] = None
self._content_eof = 0
self._cache: Dict[str, Any] = {}
self._max_decompress_size = max_decompress_size
def __aiter__(self: Self) -> Self:
return self
@@ -314,7 +325,7 @@ class BodyPartReader:
while not self._at_eof:
data.extend(await self.read_chunk(self.chunk_size))
if decode:
return self.decode(data)
return await self.decode(data)
return data
async def read_chunk(self, size: int = chunk_size) -> bytes:
@@ -357,11 +368,8 @@ class BodyPartReader:
self._read_bytes += len(chunk)
if self._read_bytes == self._length:
self._at_eof = True
if self._at_eof:
clrf = await self._content.readline()
assert (
b"\r\n" == clrf
), "reader did not read all the data or it is malformed"
if self._at_eof and await self._content.readline() != b"\r\n":
raise ValueError("Reader did not read all the data or it is malformed")
return chunk
async def _read_chunk_from_length(self, size: int) -> bytes:
@@ -382,7 +390,8 @@ class BodyPartReader:
), "Chunk size must be greater or equal than boundary length + 2"
first_chunk = self._prev_chunk is None
if first_chunk:
self._prev_chunk = await self._content.read(size)
# We need to re-add the CRLF that got removed from headers parsing.
self._prev_chunk = b"\r\n" + await self._content.read(size)
chunk = b""
# content.read() may return less than size, so we need to loop to ensure
@@ -390,7 +399,8 @@ class BodyPartReader:
while len(chunk) < self._boundary_len:
chunk += await self._content.read(size)
self._content_eof += int(self._content.at_eof())
assert self._content_eof < 3, "Reading after EOF"
if self._content_eof > 2:
raise ValueError("Reading after EOF")
if self._content_eof:
break
if len(chunk) > size:
@@ -409,12 +419,11 @@ class BodyPartReader:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
self._content.unread_data(window[idx:])
if size > idx:
self._prev_chunk = self._prev_chunk[:idx]
self._prev_chunk = self._prev_chunk[:idx]
chunk = window[len(self._prev_chunk) : idx]
if not chunk:
self._at_eof = True
result = self._prev_chunk
result = self._prev_chunk[2 if first_chunk else 0 :] # Strip initial CRLF
self._prev_chunk = chunk
return result
@@ -494,7 +503,7 @@ class BodyPartReader:
"""Returns True if the boundary was reached or False otherwise."""
return self._at_eof
def decode(self, data: bytes) -> bytes:
async def decode(self, data: bytes) -> bytes:
"""Decodes data.
Decoding is done according the specified Content-Encoding
@@ -504,18 +513,18 @@ class BodyPartReader:
data = self._decode_content_transfer(data)
# https://datatracker.ietf.org/doc/html/rfc7578#section-4.8
if not self._is_form_data and CONTENT_ENCODING in self.headers:
return self._decode_content(data)
return await self._decode_content(data)
return data
def _decode_content(self, data: bytes) -> bytes:
async def _decode_content(self, data: bytes) -> bytes:
encoding = self.headers.get(CONTENT_ENCODING, "").lower()
if encoding == "identity":
return data
if encoding in {"deflate", "gzip"}:
return ZLibDecompressor(
return await ZLibDecompressor(
encoding=encoding,
suppress_deflate_header=True,
).decompress_sync(data)
).decompress(data, max_length=self._max_decompress_size)
raise RuntimeError(f"unknown content encoding: {encoding}")
@@ -586,11 +595,11 @@ class BodyPartReaderPayload(Payload):
"""
raise TypeError("Unable to read body part as bytes. Use write() to consume.")
async def write(self, writer: Any) -> None:
async def write(self, writer: AbstractStreamWriter) -> None:
field = self._value
chunk = await field.read_chunk(size=2**16)
while chunk:
await writer.write(field.decode(chunk))
await writer.write(await field.decode(chunk))
chunk = await field.read_chunk(size=2**16)
@@ -777,10 +786,10 @@ class MultipartReader:
raise ValueError(f"Invalid boundary {chunk!r}, expected {self._boundary!r}")
async def _read_headers(self) -> "CIMultiDictProxy[str]":
lines = [b""]
lines = []
while True:
chunk = await self._content.readline()
chunk = chunk.strip()
chunk = chunk.rstrip(b"\r\n")
lines.append(chunk)
if not chunk:
break
@@ -972,14 +981,15 @@ class MultipartWriter(Payload):
"""Size of the payload."""
total = 0
for part, encoding, te_encoding in self._parts:
if encoding or te_encoding or part.size is None:
part_size = part.size
if encoding or te_encoding or part_size is None:
return None
total += int(
2
+ len(self._boundary)
+ 2
+ part.size # b'--'+self._boundary+b'\r\n'
+ part_size # b'--'+self._boundary+b'\r\n'
+ len(part._binary_headers)
+ 2 # b'\r\n'
)
@@ -1029,7 +1039,9 @@ class MultipartWriter(Payload):
return b"".join(parts)
async def write(self, writer: Any, close_boundary: bool = True) -> None:
async def write(
self, writer: AbstractStreamWriter, close_boundary: bool = True
) -> None:
"""Write body."""
for part, encoding, te_encoding in self._parts:
if self._is_form_data:
@@ -1083,7 +1095,7 @@ class MultipartWriter(Payload):
class MultipartPayloadWriter:
def __init__(self, writer: Any) -> None:
def __init__(self, writer: AbstractStreamWriter) -> None:
self._writer = writer
self._encoding: Optional[str] = None
self._compress: Optional[ZLibCompressor] = None
+23 -4
View File
@@ -486,10 +486,14 @@ class IOBasePayload(Payload):
if self._start_position is None:
try:
self._start_position = self._value.tell()
except OSError:
except (OSError, AttributeError):
self._consumed = True # Cannot seek, mark as consumed
return
self._value.seek(self._start_position)
try:
self._value.seek(self._start_position)
except (OSError, AttributeError):
# Failed to seek back - mark as consumed since we've already read
self._consumed = True
def _read_and_available_len(
self, remaining_content_len: Optional[int]
@@ -540,11 +544,26 @@ class IOBasePayload(Payload):
"""
Size of the payload in bytes.
Returns the number of bytes remaining to be read from the file.
Returns the total size of the payload content from the initial position.
This ensures consistent Content-Length for requests, including 307/308 redirects
where the same payload instance is reused.
Returns None if the size cannot be determined (e.g., for unseekable streams).
"""
try:
return os.fstat(self._value.fileno()).st_size - self._value.tell()
# Store the start position on first access.
# This is critical when the same payload instance is reused (e.g., 307/308
# redirects). Without storing the initial position, after the payload is
# read once, the file position would be at EOF, which would cause the
# size calculation to return 0 (file_size - EOF position).
# By storing the start position, we ensure the size calculation always
# returns the correct total size for any subsequent use.
if self._start_position is None:
self._start_position = self._value.tell()
# Return the total size from the start position
# This ensures Content-Length is correct even after reading
return os.fstat(self._value.fileno()).st_size - self._start_position
except (AttributeError, OSError):
return None
+36 -5
View File
@@ -116,6 +116,8 @@ class StreamReader(AsyncStreamReaderMixin):
"_protocol",
"_low_water",
"_high_water",
"_low_water_chunks",
"_high_water_chunks",
"_loop",
"_size",
"_cursor",
@@ -130,6 +132,7 @@ class StreamReader(AsyncStreamReaderMixin):
"_eof_callbacks",
"_eof_counter",
"total_bytes",
"total_compressed_bytes",
)
def __init__(
@@ -145,10 +148,15 @@ class StreamReader(AsyncStreamReaderMixin):
self._high_water = limit * 2
if loop is None:
loop = asyncio.get_event_loop()
# Ensure high_water_chunks >= 3 so it's always > low_water_chunks.
self._high_water_chunks = max(3, limit // 4)
# Use max(2, ...) because there's always at least 1 chunk split remaining
# (the current position), so we need low_water >= 2 to allow resume.
self._low_water_chunks = max(2, self._high_water_chunks // 2)
self._loop = loop
self._size = 0
self._cursor = 0
self._http_chunk_splits: Optional[List[int]] = None
self._http_chunk_splits: Optional[Deque[int]] = None
self._buffer: Deque[bytes] = collections.deque()
self._buffer_offset = 0
self._eof = False
@@ -159,6 +167,7 @@ class StreamReader(AsyncStreamReaderMixin):
self._eof_callbacks: List[Callable[[], None]] = []
self._eof_counter = 0
self.total_bytes = 0
self.total_compressed_bytes: Optional[int] = None
def __repr__(self) -> str:
info = [self.__class__.__name__]
@@ -250,6 +259,12 @@ class StreamReader(AsyncStreamReaderMixin):
finally:
self._eof_waiter = None
@property
def total_raw_bytes(self) -> int:
if self.total_compressed_bytes is None:
return self.total_bytes
return self.total_compressed_bytes
def unread_data(self, data: bytes) -> None:
"""rollback reading some data from stream, inserting it to buffer head."""
warnings.warn(
@@ -295,7 +310,7 @@ class StreamReader(AsyncStreamReaderMixin):
raise RuntimeError(
"Called begin_http_chunk_receiving when some data was already fed"
)
self._http_chunk_splits = []
self._http_chunk_splits = collections.deque()
def end_http_chunk_receiving(self) -> None:
if self._http_chunk_splits is None:
@@ -321,6 +336,15 @@ class StreamReader(AsyncStreamReaderMixin):
self._http_chunk_splits.append(self.total_bytes)
# If we get too many small chunks before self._high_water is reached, then any
# .read() call becomes computationally expensive, and could block the event loop
# for too long, hence an additional self._high_water_chunks here.
if (
len(self._http_chunk_splits) > self._high_water_chunks
and not self._protocol._reading_paused
):
self._protocol.pause_reading()
# wake up readchunk when end of http chunk received
waiter = self._waiter
if waiter is not None:
@@ -454,7 +478,7 @@ class StreamReader(AsyncStreamReaderMixin):
raise self._exception
while self._http_chunk_splits:
pos = self._http_chunk_splits.pop(0)
pos = self._http_chunk_splits.popleft()
if pos == self._cursor:
return (b"", True)
if pos > self._cursor:
@@ -527,9 +551,16 @@ class StreamReader(AsyncStreamReaderMixin):
chunk_splits = self._http_chunk_splits
# Prevent memory leak: drop useless chunk splits
while chunk_splits and chunk_splits[0] < self._cursor:
chunk_splits.pop(0)
chunk_splits.popleft()
if self._size < self._low_water and self._protocol._reading_paused:
if (
self._protocol._reading_paused
and self._size < self._low_water
and (
self._http_chunk_splits is None
or len(self._http_chunk_splits) < self._low_water_chunks
)
):
self._protocol.resume_reading()
return data
+43 -58
View File
@@ -1,5 +1,5 @@
from types import SimpleNamespace
from typing import TYPE_CHECKING, Awaitable, Mapping, Optional, Protocol, Type, TypeVar
from typing import TYPE_CHECKING, Mapping, Optional, Type, TypeVar
import attr
from aiosignal import Signal
@@ -12,14 +12,7 @@ if TYPE_CHECKING:
from .client import ClientSession
_ParamT_contra = TypeVar("_ParamT_contra", contravariant=True)
class _SignalCallback(Protocol[_ParamT_contra]):
def __call__(
self,
__client_session: ClientSession,
__trace_config_ctx: SimpleNamespace,
__params: _ParamT_contra,
) -> Awaitable[None]: ...
_TracingSignal = Signal[ClientSession, SimpleNamespace, _ParamT_contra]
__all__ = (
@@ -49,54 +42,46 @@ class TraceConfig:
def __init__(
self, trace_config_ctx_factory: Type[SimpleNamespace] = SimpleNamespace
) -> None:
self._on_request_start: Signal[_SignalCallback[TraceRequestStartParams]] = (
self._on_request_start: _TracingSignal[TraceRequestStartParams] = Signal(self)
self._on_request_chunk_sent: _TracingSignal[TraceRequestChunkSentParams] = (
Signal(self)
)
self._on_request_chunk_sent: Signal[
_SignalCallback[TraceRequestChunkSentParams]
self._on_response_chunk_received: _TracingSignal[
TraceResponseChunkReceivedParams
] = Signal(self)
self._on_response_chunk_received: Signal[
_SignalCallback[TraceResponseChunkReceivedParams]
] = Signal(self)
self._on_request_end: Signal[_SignalCallback[TraceRequestEndParams]] = Signal(
self._on_request_end: _TracingSignal[TraceRequestEndParams] = Signal(self)
self._on_request_exception: _TracingSignal[TraceRequestExceptionParams] = (
Signal(self)
)
self._on_request_redirect: _TracingSignal[TraceRequestRedirectParams] = Signal(
self
)
self._on_request_exception: Signal[
_SignalCallback[TraceRequestExceptionParams]
self._on_connection_queued_start: _TracingSignal[
TraceConnectionQueuedStartParams
] = Signal(self)
self._on_request_redirect: Signal[
_SignalCallback[TraceRequestRedirectParams]
self._on_connection_queued_end: _TracingSignal[
TraceConnectionQueuedEndParams
] = Signal(self)
self._on_connection_queued_start: Signal[
_SignalCallback[TraceConnectionQueuedStartParams]
self._on_connection_create_start: _TracingSignal[
TraceConnectionCreateStartParams
] = Signal(self)
self._on_connection_queued_end: Signal[
_SignalCallback[TraceConnectionQueuedEndParams]
self._on_connection_create_end: _TracingSignal[
TraceConnectionCreateEndParams
] = Signal(self)
self._on_connection_create_start: Signal[
_SignalCallback[TraceConnectionCreateStartParams]
self._on_connection_reuseconn: _TracingSignal[
TraceConnectionReuseconnParams
] = Signal(self)
self._on_connection_create_end: Signal[
_SignalCallback[TraceConnectionCreateEndParams]
self._on_dns_resolvehost_start: _TracingSignal[
TraceDnsResolveHostStartParams
] = Signal(self)
self._on_connection_reuseconn: Signal[
_SignalCallback[TraceConnectionReuseconnParams]
] = Signal(self)
self._on_dns_resolvehost_start: Signal[
_SignalCallback[TraceDnsResolveHostStartParams]
] = Signal(self)
self._on_dns_resolvehost_end: Signal[
_SignalCallback[TraceDnsResolveHostEndParams]
] = Signal(self)
self._on_dns_cache_hit: Signal[_SignalCallback[TraceDnsCacheHitParams]] = (
self._on_dns_resolvehost_end: _TracingSignal[TraceDnsResolveHostEndParams] = (
Signal(self)
)
self._on_dns_cache_miss: Signal[_SignalCallback[TraceDnsCacheMissParams]] = (
self._on_dns_cache_hit: _TracingSignal[TraceDnsCacheHitParams] = Signal(self)
self._on_dns_cache_miss: _TracingSignal[TraceDnsCacheMissParams] = Signal(self)
self._on_request_headers_sent: _TracingSignal[TraceRequestHeadersSentParams] = (
Signal(self)
)
self._on_request_headers_sent: Signal[
_SignalCallback[TraceRequestHeadersSentParams]
] = Signal(self)
self._trace_config_ctx_factory = trace_config_ctx_factory
@@ -125,91 +110,91 @@ class TraceConfig:
self._on_request_headers_sent.freeze()
@property
def on_request_start(self) -> "Signal[_SignalCallback[TraceRequestStartParams]]":
def on_request_start(self) -> "_TracingSignal[TraceRequestStartParams]":
return self._on_request_start
@property
def on_request_chunk_sent(
self,
) -> "Signal[_SignalCallback[TraceRequestChunkSentParams]]":
) -> "_TracingSignal[TraceRequestChunkSentParams]":
return self._on_request_chunk_sent
@property
def on_response_chunk_received(
self,
) -> "Signal[_SignalCallback[TraceResponseChunkReceivedParams]]":
) -> "_TracingSignal[TraceResponseChunkReceivedParams]":
return self._on_response_chunk_received
@property
def on_request_end(self) -> "Signal[_SignalCallback[TraceRequestEndParams]]":
def on_request_end(self) -> "_TracingSignal[TraceRequestEndParams]":
return self._on_request_end
@property
def on_request_exception(
self,
) -> "Signal[_SignalCallback[TraceRequestExceptionParams]]":
) -> "_TracingSignal[TraceRequestExceptionParams]":
return self._on_request_exception
@property
def on_request_redirect(
self,
) -> "Signal[_SignalCallback[TraceRequestRedirectParams]]":
) -> "_TracingSignal[TraceRequestRedirectParams]":
return self._on_request_redirect
@property
def on_connection_queued_start(
self,
) -> "Signal[_SignalCallback[TraceConnectionQueuedStartParams]]":
) -> "_TracingSignal[TraceConnectionQueuedStartParams]":
return self._on_connection_queued_start
@property
def on_connection_queued_end(
self,
) -> "Signal[_SignalCallback[TraceConnectionQueuedEndParams]]":
) -> "_TracingSignal[TraceConnectionQueuedEndParams]":
return self._on_connection_queued_end
@property
def on_connection_create_start(
self,
) -> "Signal[_SignalCallback[TraceConnectionCreateStartParams]]":
) -> "_TracingSignal[TraceConnectionCreateStartParams]":
return self._on_connection_create_start
@property
def on_connection_create_end(
self,
) -> "Signal[_SignalCallback[TraceConnectionCreateEndParams]]":
) -> "_TracingSignal[TraceConnectionCreateEndParams]":
return self._on_connection_create_end
@property
def on_connection_reuseconn(
self,
) -> "Signal[_SignalCallback[TraceConnectionReuseconnParams]]":
) -> "_TracingSignal[TraceConnectionReuseconnParams]":
return self._on_connection_reuseconn
@property
def on_dns_resolvehost_start(
self,
) -> "Signal[_SignalCallback[TraceDnsResolveHostStartParams]]":
) -> "_TracingSignal[TraceDnsResolveHostStartParams]":
return self._on_dns_resolvehost_start
@property
def on_dns_resolvehost_end(
self,
) -> "Signal[_SignalCallback[TraceDnsResolveHostEndParams]]":
) -> "_TracingSignal[TraceDnsResolveHostEndParams]":
return self._on_dns_resolvehost_end
@property
def on_dns_cache_hit(self) -> "Signal[_SignalCallback[TraceDnsCacheHitParams]]":
def on_dns_cache_hit(self) -> "_TracingSignal[TraceDnsCacheHitParams]":
return self._on_dns_cache_hit
@property
def on_dns_cache_miss(self) -> "Signal[_SignalCallback[TraceDnsCacheMissParams]]":
def on_dns_cache_miss(self) -> "_TracingSignal[TraceDnsCacheMissParams]":
return self._on_dns_cache_miss
@property
def on_request_headers_sent(
self,
) -> "Signal[_SignalCallback[TraceRequestHeadersSentParams]]":
) -> "_TracingSignal[TraceRequestHeadersSentParams]":
return self._on_request_headers_sent
+4 -17
View File
@@ -309,18 +309,12 @@ async def _run_app(
port: Optional[int] = None,
path: Union[PathLike, TypingIterable[PathLike], None] = None,
sock: Optional[Union[socket.socket, TypingIterable[socket.socket]]] = None,
shutdown_timeout: float = 60.0,
keepalive_timeout: float = 75.0,
ssl_context: Optional[SSLContext] = None,
print: Optional[Callable[..., None]] = print,
backlog: int = 128,
access_log_class: Type[AbstractAccessLogger] = AccessLogger,
access_log_format: str = AccessLogger.LOG_FORMAT,
access_log: Optional[logging.Logger] = access_logger,
handle_signals: bool = True,
reuse_address: Optional[bool] = None,
reuse_port: Optional[bool] = None,
handler_cancellation: bool = False,
**kwargs: Any, # TODO(PY311): Use Unpack
) -> None:
# An internal function to actually do all dirty job for application running
if asyncio.iscoroutine(app):
@@ -328,16 +322,7 @@ async def _run_app(
app = cast(Application, app)
runner = AppRunner(
app,
handle_signals=handle_signals,
access_log_class=access_log_class,
access_log_format=access_log_format,
access_log=access_log,
keepalive_timeout=keepalive_timeout,
shutdown_timeout=shutdown_timeout,
handler_cancellation=handler_cancellation,
)
runner = AppRunner(app, **kwargs)
await runner.setup()
@@ -484,6 +469,7 @@ def run_app(
reuse_port: Optional[bool] = None,
handler_cancellation: bool = False,
loop: Optional[asyncio.AbstractEventLoop] = None,
**kwargs: Any,
) -> None:
"""Run an app locally"""
if loop is None:
@@ -515,6 +501,7 @@ def run_app(
reuse_address=reuse_address,
reuse_port=reuse_port,
handler_cancellation=handler_cancellation,
**kwargs,
)
)
+2 -2
View File
@@ -62,8 +62,8 @@ __all__ = ("Application", "CleanupError")
if TYPE_CHECKING:
_AppSignal = Signal[Callable[["Application"], Awaitable[None]]]
_RespPrepareSignal = Signal[Callable[[Request, StreamResponse], Awaitable[None]]]
_AppSignal = Signal["Application"]
_RespPrepareSignal = Signal[Request, StreamResponse]
_Middlewares = FrozenList[Middleware]
_MiddlewaresHandlers = Optional[Sequence[Tuple[Middleware, bool]]]
_Subapps = List["Application"]
+4 -4
View File
@@ -164,8 +164,8 @@ class FileResponse(StreamResponse):
) -> Optional[AbstractStreamWriter]:
self.set_status(HTTPNotModified.status_code)
self._length_check = False
self.etag = etag_value # type: ignore[assignment]
self.last_modified = last_modified # type: ignore[assignment]
self.etag = etag_value
self.last_modified = last_modified
# Delete any Content-Length headers provided by user. HTTP 304
# should always have empty response body
return await super().prepare(request)
@@ -395,8 +395,8 @@ class FileResponse(StreamResponse):
# compress.
self._compression = False
self.etag = f"{st.st_mtime_ns:x}-{st.st_size:x}" # type: ignore[assignment]
self.last_modified = file_mtime # type: ignore[assignment]
self.etag = f"{st.st_mtime_ns:x}-{st.st_size:x}"
self.last_modified = file_mtime
self.content_length = count
self._headers[hdrs.ACCEPT_RANGES] = "bytes"
+1 -1
View File
@@ -468,7 +468,7 @@ class RequestHandler(BaseProtocol):
def log_access(
self, request: BaseRequest, response: StreamResponse, time: Optional[float]
) -> None:
if self.access_logger is not None and self.access_logger.enabled:
if self._logging_enabled and self.access_logger is not None:
if TYPE_CHECKING:
assert time is not None
self.access_logger.log(request, response, self._loop.time() - time)
+6 -8
View File
@@ -607,7 +607,7 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin):
if rng is not None:
try:
pattern = r"^bytes=(\d*)-(\d*)$"
start, end = re.findall(pattern, rng)[0]
start, end = re.findall(pattern, rng, re.ASCII)[0]
except IndexError: # pattern was not found in header
raise ValueError("range not in acceptable format")
@@ -721,13 +721,13 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin):
multipart = await self.multipart()
max_size = self._client_max_size
field = await multipart.next()
while field is not None:
size = 0
size = 0
while (field := await multipart.next()) is not None:
field_ct = field.headers.get(hdrs.CONTENT_TYPE)
if isinstance(field, BodyPartReader):
assert field.name is not None
if field.name is None:
raise ValueError("Multipart field missing name.")
# Note that according to RFC 7578, the Content-Type header
# is optional, even for files, so we can't assume it's
@@ -740,7 +740,7 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin):
)
chunk = await field.read_chunk(size=2**16)
while chunk:
chunk = field.decode(chunk)
chunk = await field.decode(chunk)
await self._loop.run_in_executor(None, tmp.write, chunk)
size += len(chunk)
if 0 < max_size < size:
@@ -779,8 +779,6 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin):
raise ValueError(
"To decode nested multipart you need to use custom reader",
)
field = await multipart.next()
else:
data = await self.read()
if data:
+2 -2
View File
@@ -791,8 +791,8 @@ class Response(StreamResponse):
del self._headers[hdrs.CONTENT_LENGTH]
elif not self._chunked:
if isinstance(self._body, Payload):
if self._body.size is not None:
self._headers[hdrs.CONTENT_LENGTH] = str(self._body.size)
if (size := self._body.size) is not None:
self._headers[hdrs.CONTENT_LENGTH] = str(size)
else:
body_len = len(self._body) if self._body else "0"
# https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-7
+27 -25
View File
@@ -7,6 +7,7 @@ import html
import inspect
import keyword
import os
import platform
import re
import sys
import warnings
@@ -94,6 +95,7 @@ ROUTE_RE: Final[Pattern[str]] = re.compile(
)
PATH_SEP: Final[str] = re.escape("/")
IS_WINDOWS: Final[bool] = platform.system() == "Windows"
_ExpectHandler = Callable[[Request], Awaitable[Optional[StreamResponse]]]
_Resolve = Tuple[Optional["UrlMappingMatchInfo"], Set[str]]
@@ -194,6 +196,8 @@ class AbstractRoute(abc.ABC):
):
pass
elif inspect.isgeneratorfunction(handler):
if TYPE_CHECKING:
assert False
warnings.warn(
"Bare generators are deprecated, use @coroutine wrapper",
DeprecationWarning,
@@ -649,7 +653,12 @@ class StaticResource(PrefixResource):
async def resolve(self, request: Request) -> _Resolve:
path = request.rel_url.path_safe
method = request.method
if not path.startswith(self._prefix2) and path != self._prefix:
# We normalise here to avoid matches that traverse below the static root.
# e.g. /static/../../../../home/user/webapp/static/
norm_path = os.path.normpath(path)
if IS_WINDOWS:
norm_path = norm_path.replace("\\", "/")
if not norm_path.startswith(self._prefix2) and norm_path != self._prefix:
return None, set()
allowed_methods = self._allowed_methods
@@ -666,14 +675,7 @@ class StaticResource(PrefixResource):
return iter(self._routes.values())
async def _handle(self, request: Request) -> StreamResponse:
rel_url = request.match_info["filename"]
filename = Path(rel_url)
if filename.anchor:
# rel_url is an absolute name like
# /static/\\machine_name\c$ or /static/D:\path
# where the static dir is totally different
raise HTTPForbidden()
filename = request.match_info["filename"]
unresolved_path = self._directory.joinpath(filename)
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
@@ -978,7 +980,7 @@ class View(AbstractView):
assert isinstance(ret, StreamResponse)
return ret
def __await__(self) -> Generator[Any, None, StreamResponse]:
def __await__(self) -> Generator[None, None, StreamResponse]:
return self._iter().__await__()
def _raise_allowed_methods(self) -> NoReturn:
@@ -1032,6 +1034,21 @@ class UrlDispatcher(AbstractRouter, Mapping[str, AbstractResource]):
resource_index = self._resource_index
allowed_methods: Set[str] = set()
# MatchedSubAppResource is primarily used to match on domain names
# (though custom rules could match on other things). This means that
# the traversal algorithm below can't be applied, and that we likely
# need to check these first so a sub app that defines the same path
# as a parent app will get priority if there's a domain match.
#
# For most cases we do not expect there to be many of these since
# currently they are only added by `.add_domain()`.
for resource in self._matched_sub_app_resources:
match_dict, allowed = await resource.resolve(request)
if match_dict is not None:
return match_dict
else:
allowed_methods |= allowed
# Walk the url parts looking for candidates. We walk the url backwards
# to ensure the most explicit match is found first. If there are multiple
# candidates for a given url part because there are multiple resources
@@ -1049,21 +1066,6 @@ class UrlDispatcher(AbstractRouter, Mapping[str, AbstractResource]):
break
url_part = url_part.rpartition("/")[0] or "/"
#
# We didn't find any candidates, so we'll try the matched sub-app
# resources which we have to walk in a linear fashion because they
# have regex/wildcard match rules and we cannot index them.
#
# For most cases we do not expect there to be many of these since
# currently they are only added by `add_domain`
#
for resource in self._matched_sub_app_resources:
match_dict, allowed = await resource.resolve(request)
if match_dict is not None:
return match_dict
else:
allowed_methods |= allowed
if allowed_methods:
return MatchInfoError(HTTPMethodNotAllowed(request.method, allowed_methods))
+85 -6
View File
@@ -1,10 +1,94 @@
[build-system]
requires = [
"pkgconfig",
"setuptools >= 46.4.0",
# setuptools >= 67.0 required for Python 3.12+ support
# Next step should be >= 77.0 for PEP 639 support
# Don't bump too early to give distributors time to update
# their setuptools version.
"setuptools >= 67.0",
]
build-backend = "setuptools.build_meta"
[project]
name = "aiohttp"
# TODO: Update to just 'license = "..."' once setuptools is bumped to >=77
license = {text = "Apache-2.0 AND MIT"}
description = "Async http client/server framework (asyncio)"
readme = "README.rst"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"Operating System :: POSIX",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Internet :: WWW/HTTP",
]
requires-python = ">= 3.9"
dependencies = [
"aiohappyeyeballs >= 2.5.0",
"aiosignal >= 1.4.0",
"async-timeout >= 4.0, < 6.0 ; python_version < '3.11'",
"attrs >= 17.3.0",
"frozenlist >= 1.1.1",
"multidict >=4.5, < 7.0",
"propcache >= 0.2.0",
"yarl >= 1.17.0, < 2.0",
]
dynamic = [
"version",
]
[project.optional-dependencies]
speedups = [
"aiodns >= 3.3.0",
"Brotli >= 1.2; platform_python_implementation == 'CPython'",
"brotlicffi >= 1.2; platform_python_implementation != 'CPython'",
"backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14'",
]
[[project.maintainers]]
name = "aiohttp team"
email = "team@aiohttp.org"
[project.urls]
"Homepage" = "https://github.com/aio-libs/aiohttp"
"Chat: Matrix" = "https://matrix.to/#/#aio-libs:matrix.org"
"Chat: Matrix Space" = "https://matrix.to/#/#aio-libs-space:matrix.org"
"CI: GitHub Actions" = "https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI"
"Coverage: codecov" = "https://codecov.io/github/aio-libs/aiohttp"
"Docs: Changelog" = "https://docs.aiohttp.org/en/stable/changes.html"
"Docs: RTD" = "https://docs.aiohttp.org"
"GitHub: issues" = "https://github.com/aio-libs/aiohttp/issues"
"GitHub: repo" = "https://github.com/aio-libs/aiohttp"
[tool.setuptools]
license-files = [
# TODO: Use 'project.license-files' instead once setuptools is bumped to >=77
"LICENSE.txt",
"vendor/llhttp/LICENSE",
]
[tool.setuptools.dynamic]
version = {attr = "aiohttp.__version__"}
[tool.setuptools.packages.find]
include = [
"aiohttp",
"aiohttp.*",
]
[tool.setuptools.exclude-package-data]
"*" = ["*.c", "*.h"]
[tool.towncrier]
package = "aiohttp"
filename = "CHANGES.rst"
@@ -88,8 +172,3 @@ ignore-words-list = 'te,ue'
# TODO(3.13): Remove aiohttp.helpers once https://github.com/python/cpython/pull/106771
# is available in all supported cpython versions
exclude-modules = "(^aiohttp\\.helpers)"
[tool.black]
# TODO: Remove when project metadata is moved here.
# Black can read the value from [project.requires-python].
target-version = ["py39", "py310", "py311", "py312"]
@@ -1 +1 @@
fdc695f12e2e48e0f73319168d74ce5fa039801e0e83c3d430e78156162d3882 /home/runner/work/aiohttp/aiohttp/requirements/cython.txt
be58a7c23eaf4532a259b5ac86ace4c1bf9e55e67da7e4afec9dc55e8ed84597 /home/runner/work/aiohttp/aiohttp/requirements/cython.txt
+3
View File
@@ -0,0 +1,3 @@
-r runtime-deps.in
gunicorn
+50
View File
@@ -0,0 +1,50 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/base-ft.txt --strip-extras requirements/base-ft.in
#
aiodns==3.5.0
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
aiosignal==1.4.0
# via -r requirements/runtime-deps.in
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
attrs==25.3.0
# via -r requirements/runtime-deps.in
brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
cffi==2.0.0
# via pycares
frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
gunicorn==23.0.0
# via -r requirements/base-ft.in
idna==3.10
# via yarl
multidict==6.6.4
# via
# -r requirements/runtime-deps.in
# yarl
packaging==25.0
# via gunicorn
propcache==0.4.0
# via
# -r requirements/runtime-deps.in
# yarl
pycares==4.11.0
# via aiodns
pycparser==2.23
# via cffi
typing-extensions==4.15.0
# via
# aiosignal
# multidict
yarl==1.21.0
# via -r requirements/runtime-deps.in
backports.zstd==0.5.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
# via -r requirements/runtime-deps.in
+16 -13
View File
@@ -4,21 +4,21 @@
#
# pip-compile --allow-unsafe --output-file=requirements/base.txt --strip-extras requirements/base.in
#
aiodns==3.4.0
aiodns==3.5.0
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
aiosignal==1.3.2
aiosignal==1.4.0
# via -r requirements/runtime-deps.in
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
attrs==25.3.0
# via -r requirements/runtime-deps.in
brotli==1.1.0 ; platform_python_implementation == "CPython"
brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
cffi==1.17.1
cffi==2.0.0
# via pycares
frozenlist==1.6.0
frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
@@ -26,24 +26,27 @@ gunicorn==23.0.0
# via -r requirements/base.in
idna==3.4
# via yarl
multidict==6.4.4
multidict==6.6.4
# via
# -r requirements/runtime-deps.in
# yarl
packaging==25.0
# via gunicorn
propcache==0.3.1
propcache==0.4.0
# via
# -r requirements/runtime-deps.in
# yarl
pycares==4.8.0
pycares==4.11.0
# via aiodns
pycparser==2.22
pycparser==2.23
# via cffi
typing-extensions==4.13.2
# via multidict
typing-extensions==4.15.0
# via
# aiosignal
# multidict
uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpython"
winloop==0.1.8; platform_system == "Windows" and implementation_name == "cpython"
# via -r requirements/base.in
yarl==1.20.0
yarl==1.21.0
# via -r requirements/runtime-deps.in
backports.zstd==0.5.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
# via -r requirements/runtime-deps.in
+88 -82
View File
@@ -1,10 +1,10 @@
#
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/constraints.txt --resolver=backtracking --strip-extras requirements/constraints.in
# pip-compile --allow-unsafe --output-file=requirements/constraints.txt --strip-extras requirements/constraints.in
#
aiodns==3.4.0
aiodns==3.5.0
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
@@ -12,7 +12,7 @@ aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
aiohttp-theme==0.1.7
# via -r requirements/doc.in
aiosignal==1.3.2
aiosignal==1.4.0
# via -r requirements/runtime-deps.in
alabaster==1.0.0
# via sphinx
@@ -26,26 +26,26 @@ attrs==25.3.0
# via -r requirements/runtime-deps.in
babel==2.17.0
# via sphinx
blockbuster==1.5.24
blockbuster==1.5.26
# via
# -r requirements/lint.in
# -r requirements/test.in
brotli==1.1.0 ; platform_python_implementation == "CPython"
# -r requirements/test-common.in
brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
build==1.2.2.post1
build==1.3.0
# via pip-tools
certifi==2025.4.26
certifi==2025.10.5
# via requests
cffi==1.17.1
cffi==2.0.0
# via
# cryptography
# pycares
# pytest-codspeed
cfgv==3.4.0
# via pre-commit
charset-normalizer==3.4.2
charset-normalizer==3.4.3
# via requests
cherry-picker==2.5.0
cherry-picker==2.6.0
# via -r requirements/dev.in
click==8.1.8
# via
@@ -54,17 +54,17 @@ click==8.1.8
# slotscheck
# towncrier
# wait-for-it
coverage==7.8.1
coverage==7.10.7
# via
# -r requirements/test.in
# -r requirements/test-common.in
# pytest-cov
cryptography==45.0.2
cryptography==46.0.2
# via
# pyjwt
# trustme
cython==3.1.1
cython==3.1.4
# via -r requirements/cython.in
distlib==0.3.9
distlib==0.4.0
# via virtualenv
docutils==0.21.2
# via sphinx
@@ -72,23 +72,23 @@ exceptiongroup==1.3.0
# via pytest
execnet==2.1.1
# via pytest-xdist
filelock==3.18.0
filelock==3.19.1
# via virtualenv
forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.1
freezegun==1.5.5
# via
# -r requirements/lint.in
# -r requirements/test.in
frozenlist==1.6.0
# -r requirements/test-common.in
frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
gidgethub==5.3.0
gidgethub==5.4.0
# via cherry-picker
gunicorn==23.0.0
# via -r requirements/base.in
identify==2.6.10
identify==2.6.15
# via pre-commit
idna==3.3
# via
@@ -97,33 +97,31 @@ idna==3.3
# yarl
imagesize==1.4.1
# via sphinx
incremental==24.7.2
# via towncrier
iniconfig==2.1.0
# via pytest
isal==1.7.2
isal==1.7.2 ; python_version < "3.14"
# via
# -r requirements/lint.in
# -r requirements/test.in
# -r requirements/test-common.in
jinja2==3.1.6
# via
# sphinx
# towncrier
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
markupsafe==3.0.3
# via jinja2
mdurl==0.1.2
# via markdown-it-py
multidict==6.4.4
multidict==6.6.4
# via
# -r requirements/multidict.in
# -r requirements/runtime-deps.in
# yarl
mypy==1.15.0 ; implementation_name == "cpython"
mypy==1.18.2 ; implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test.in
# -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
nodeenv==1.9.1
@@ -134,34 +132,39 @@ packaging==25.0
# gunicorn
# pytest
# sphinx
pip-tools==7.4.1
pathspec==0.12.1
# via mypy
pip-tools==7.5.1
# via -r requirements/dev.in
pkgconfig==1.5.5
# via -r requirements/test.in
platformdirs==4.3.8
# via -r requirements/test-common.in
platformdirs==4.4.0
# via virtualenv
pluggy==1.6.0
# via pytest
pre-commit==4.2.0
# via
# pytest
# pytest-cov
pre-commit==4.3.0
# via -r requirements/lint.in
propcache==0.3.1
propcache==0.4.0
# via
# -r requirements/runtime-deps.in
# yarl
proxy-py==2.4.10
# via -r requirements/test.in
pycares==4.8.0
# via -r requirements/test-common.in
pycares==4.11.0
# via aiodns
pycparser==2.22
pycparser==2.23
# via cffi
pydantic==2.11.5
pydantic==2.11.9
# via python-on-whales
pydantic-core==2.33.2
# via pydantic
pyenchant==3.2.2
pyenchant==3.3.0
# via sphinxcontrib-spelling
pygments==2.19.1
pygments==2.19.2
# via
# pytest
# rich
# sphinx
pyjwt==2.9.0
@@ -172,47 +175,47 @@ pyproject-hooks==1.2.0
# via
# build
# pip-tools
pytest==8.3.5
pytest==8.4.2
# via
# -r requirements/lint.in
# -r requirements/test.in
# -r requirements/test-common.in
# pytest-codspeed
# pytest-cov
# pytest-mock
# pytest-xdist
pytest-codspeed==3.2.0
pytest-codspeed==4.0.0
# via
# -r requirements/lint.in
# -r requirements/test.in
pytest-cov==6.1.1
# via -r requirements/test.in
pytest-mock==3.14.0
# -r requirements/test-common.in
pytest-cov==7.0.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via
# -r requirements/lint.in
# -r requirements/test.in
pytest-xdist==3.6.1
# via -r requirements/test.in
# -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
python-on-whales==0.76.1
python-on-whales==0.78.0
# via
# -r requirements/lint.in
# -r requirements/test.in
pyyaml==6.0.2
# -r requirements/test-common.in
pyyaml==6.0.3
# via pre-commit
re-assert==1.1.0
# via -r requirements/test.in
regex==2024.11.6
# via -r requirements/test-common.in
regex==2025.9.18
# via re-assert
requests==2.32.3
requests==2.32.5
# via
# cherry-picker
# sphinx
# sphinxcontrib-spelling
rich==14.0.0
rich==14.1.0
# via pytest-codspeed
setuptools-git==1.2
# via -r requirements/test.in
# via -r requirements/test-common.in
six==1.17.0
# via python-dateutil
slotscheck==0.19.1
@@ -242,67 +245,70 @@ sphinxcontrib-towncrier==0.5.0a0
# via -r requirements/doc.in
stamina==25.1.0
# via cherry-picker
tenacity==9.0.0
tenacity==9.1.2
# via stamina
tomli==2.2.1
# via
# build
# cherry-picker
# coverage
# incremental
# mypy
# pip-tools
# pytest
# slotscheck
# sphinx
# towncrier
towncrier==23.11.0
towncrier==25.8.0
# via
# -r requirements/doc.in
# sphinxcontrib-towncrier
trustme==1.2.1 ; platform_machine != "i686"
# via
# -r requirements/lint.in
# -r requirements/test.in
typing-extensions==4.13.2
# -r requirements/test-common.in
typing-extensions==4.15.0
# via
# aiosignal
# cryptography
# exceptiongroup
# multidict
# mypy
# pydantic
# pydantic-core
# python-on-whales
# rich
# typing-inspection
typing-inspection==0.4.1
# virtualenv
typing-inspection==0.4.2
# via pydantic
uritemplate==4.1.1
uritemplate==4.2.0
# via gidgethub
urllib3==2.4.0
urllib3==2.5.0
# via requests
uvloop==0.21.0 ; platform_system != "Windows"
# via
# -r requirements/base.in
# -r requirements/lint.in
valkey==6.1.0
valkey==6.1.1
# via -r requirements/lint.in
virtualenv==20.31.2
virtualenv==20.34.0
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test.in
# via -r requirements/test-common.in
wheel==0.45.1
# via pip-tools
yarl==1.20.0
yarl==1.21.0
# via -r requirements/runtime-deps.in
zlib-ng==0.5.1
zlib-ng==1.0.0
# via
# -r requirements/lint.in
# -r requirements/test.in
# -r requirements/test-common.in
backports.zstd==0.5.0 ; implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
# The following packages are considered to be unsafe in a requirements file:
pip==25.1.1
pip==25.2
# via pip-tools
setuptools==80.9.0
# via pip-tools
setuptools==80.8.0
# via
# incremental
# pip-tools
+3 -3
View File
@@ -4,9 +4,9 @@
#
# pip-compile --allow-unsafe --output-file=requirements/cython.txt --resolver=backtracking --strip-extras requirements/cython.in
#
cython==3.1.1
cython==3.1.4
# via -r requirements/cython.in
multidict==6.4.4
multidict==6.6.4
# via -r requirements/multidict.in
typing-extensions==4.13.2
typing-extensions==4.15.0
# via multidict
+86 -80
View File
@@ -1,10 +1,10 @@
#
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/dev.txt --resolver=backtracking --strip-extras requirements/dev.in
# pip-compile --allow-unsafe --output-file=requirements/dev.txt --strip-extras requirements/dev.in
#
aiodns==3.4.0
aiodns==3.5.0
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
@@ -12,7 +12,7 @@ aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
aiohttp-theme==0.1.7
# via -r requirements/doc.in
aiosignal==1.3.2
aiosignal==1.4.0
# via -r requirements/runtime-deps.in
alabaster==1.0.0
# via sphinx
@@ -26,26 +26,26 @@ attrs==25.3.0
# via -r requirements/runtime-deps.in
babel==2.17.0
# via sphinx
blockbuster==1.5.24
blockbuster==1.5.26
# via
# -r requirements/lint.in
# -r requirements/test.in
brotli==1.1.0 ; platform_python_implementation == "CPython"
# -r requirements/test-common.in
brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
build==1.2.2.post1
build==1.3.0
# via pip-tools
certifi==2025.4.26
certifi==2025.10.5
# via requests
cffi==1.17.1
cffi==2.0.0
# via
# cryptography
# pycares
# pytest-codspeed
cfgv==3.4.0
# via pre-commit
charset-normalizer==3.4.2
charset-normalizer==3.4.3
# via requests
cherry-picker==2.5.0
cherry-picker==2.6.0
# via -r requirements/dev.in
click==8.1.8
# via
@@ -54,15 +54,15 @@ click==8.1.8
# slotscheck
# towncrier
# wait-for-it
coverage==7.8.1
coverage==7.10.7
# via
# -r requirements/test.in
# -r requirements/test-common.in
# pytest-cov
cryptography==45.0.2
cryptography==46.0.2
# via
# pyjwt
# trustme
distlib==0.3.9
distlib==0.4.0
# via virtualenv
docutils==0.21.2
# via sphinx
@@ -70,23 +70,23 @@ exceptiongroup==1.3.0
# via pytest
execnet==2.1.1
# via pytest-xdist
filelock==3.18.0
filelock==3.19.1
# via virtualenv
forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.1
freezegun==1.5.5
# via
# -r requirements/lint.in
# -r requirements/test.in
frozenlist==1.6.0
# -r requirements/test-common.in
frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
gidgethub==5.3.0
gidgethub==5.4.0
# via cherry-picker
gunicorn==23.0.0
# via -r requirements/base.in
identify==2.6.10
identify==2.6.15
# via pre-commit
idna==3.4
# via
@@ -95,32 +95,30 @@ idna==3.4
# yarl
imagesize==1.4.1
# via sphinx
incremental==24.7.2
# via towncrier
iniconfig==2.1.0
# via pytest
isal==1.7.2
isal==1.7.2 ; python_version < "3.14"
# via
# -r requirements/lint.in
# -r requirements/test.in
# -r requirements/test-common.in
jinja2==3.1.6
# via
# sphinx
# towncrier
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
markupsafe==3.0.3
# via jinja2
mdurl==0.1.2
# via markdown-it-py
multidict==6.4.4
multidict==6.6.4
# via
# -r requirements/runtime-deps.in
# yarl
mypy==1.15.0 ; implementation_name == "cpython"
mypy==1.18.2 ; implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test.in
# -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
nodeenv==1.9.1
@@ -131,32 +129,37 @@ packaging==25.0
# gunicorn
# pytest
# sphinx
pip-tools==7.4.1
pathspec==0.12.1
# via mypy
pip-tools==7.5.1
# via -r requirements/dev.in
pkgconfig==1.5.5
# via -r requirements/test.in
platformdirs==4.3.8
# via -r requirements/test-common.in
platformdirs==4.4.0
# via virtualenv
pluggy==1.6.0
# via pytest
pre-commit==4.2.0
# via
# pytest
# pytest-cov
pre-commit==4.3.0
# via -r requirements/lint.in
propcache==0.3.1
propcache==0.4.0
# via
# -r requirements/runtime-deps.in
# yarl
proxy-py==2.4.10
# via -r requirements/test.in
pycares==4.8.0
# via -r requirements/test-common.in
pycares==4.11.0
# via aiodns
pycparser==2.22
pycparser==2.23
# via cffi
pydantic==2.11.5
pydantic==2.11.9
# via python-on-whales
pydantic-core==2.33.2
# via pydantic
pygments==2.19.1
pygments==2.19.2
# via
# pytest
# rich
# sphinx
pyjwt==2.8.0
@@ -167,46 +170,46 @@ pyproject-hooks==1.2.0
# via
# build
# pip-tools
pytest==8.3.5
pytest==8.4.2
# via
# -r requirements/lint.in
# -r requirements/test.in
# -r requirements/test-common.in
# pytest-codspeed
# pytest-cov
# pytest-mock
# pytest-xdist
pytest-codspeed==3.2.0
pytest-codspeed==4.0.0
# via
# -r requirements/lint.in
# -r requirements/test.in
pytest-cov==6.1.1
# via -r requirements/test.in
pytest-mock==3.14.0
# -r requirements/test-common.in
pytest-cov==7.0.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via
# -r requirements/lint.in
# -r requirements/test.in
pytest-xdist==3.6.1
# via -r requirements/test.in
# -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
python-on-whales==0.76.1
python-on-whales==0.78.0
# via
# -r requirements/lint.in
# -r requirements/test.in
pyyaml==6.0.2
# -r requirements/test-common.in
pyyaml==6.0.3
# via pre-commit
re-assert==1.1.0
# via -r requirements/test.in
regex==2024.11.6
# via -r requirements/test-common.in
regex==2025.9.18
# via re-assert
requests==2.32.3
requests==2.32.5
# via
# cherry-picker
# sphinx
rich==14.0.0
rich==14.1.0
# via pytest-codspeed
setuptools-git==1.2
# via -r requirements/test.in
# via -r requirements/test-common.in
six==1.17.0
# via python-dateutil
slotscheck==0.19.1
@@ -233,67 +236,70 @@ sphinxcontrib-towncrier==0.5.0a0
# via -r requirements/doc.in
stamina==25.1.0
# via cherry-picker
tenacity==9.0.0
tenacity==9.1.2
# via stamina
tomli==2.2.1
# via
# build
# cherry-picker
# coverage
# incremental
# mypy
# pip-tools
# pytest
# slotscheck
# sphinx
# towncrier
towncrier==23.11.0
towncrier==25.8.0
# via
# -r requirements/doc.in
# sphinxcontrib-towncrier
trustme==1.2.1 ; platform_machine != "i686"
# via
# -r requirements/lint.in
# -r requirements/test.in
typing-extensions==4.13.2
# -r requirements/test-common.in
typing-extensions==4.15.0
# via
# aiosignal
# cryptography
# exceptiongroup
# multidict
# mypy
# pydantic
# pydantic-core
# python-on-whales
# rich
# typing-inspection
typing-inspection==0.4.1
# virtualenv
typing-inspection==0.4.2
# via pydantic
uritemplate==4.1.1
uritemplate==4.2.0
# via gidgethub
urllib3==2.4.0
urllib3==2.5.0
# via requests
uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpython"
# via
# -r requirements/base.in
# -r requirements/lint.in
valkey==6.1.0
valkey==6.1.1
# via -r requirements/lint.in
virtualenv==20.31.2
virtualenv==20.34.0
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test.in
# via -r requirements/test-common.in
wheel==0.45.1
# via pip-tools
yarl==1.20.0
yarl==1.21.0
# via -r requirements/runtime-deps.in
zlib-ng==0.5.1
zlib-ng==1.0.0
# via
# -r requirements/lint.in
# -r requirements/test.in
# -r requirements/test-common.in
backports.zstd==0.5.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
# The following packages are considered to be unsafe in a requirements file:
pip==25.1.1
pip==25.2
# via pip-tools
setuptools==80.9.0
# via pip-tools
setuptools==80.8.0
# via
# incremental
# pip-tools
+8 -15
View File
@@ -10,9 +10,9 @@ alabaster==1.0.0
# via sphinx
babel==2.17.0
# via sphinx
certifi==2025.4.26
certifi==2025.10.5
# via requests
charset-normalizer==3.4.2
charset-normalizer==3.4.3
# via requests
click==8.1.8
# via towncrier
@@ -22,21 +22,19 @@ idna==3.4
# via requests
imagesize==1.4.1
# via sphinx
incremental==24.7.2
# via towncrier
jinja2==3.1.6
# via
# sphinx
# towncrier
markupsafe==3.0.2
markupsafe==3.0.3
# via jinja2
packaging==25.0
# via sphinx
pyenchant==3.2.2
pyenchant==3.3.0
# via sphinxcontrib-spelling
pygments==2.19.1
pygments==2.19.2
# via sphinx
requests==2.32.3
requests==2.32.5
# via
# sphinx
# sphinxcontrib-spelling
@@ -65,16 +63,11 @@ sphinxcontrib-towncrier==0.5.0a0
# via -r requirements/doc.in
tomli==2.2.1
# via
# incremental
# sphinx
# towncrier
towncrier==23.11.0
towncrier==25.8.0
# via
# -r requirements/doc.in
# sphinxcontrib-towncrier
urllib3==2.4.0
urllib3==2.5.0
# via requests
# The following packages are considered to be unsafe in a requirements file:
setuptools==80.8.0
# via incremental
+8 -15
View File
@@ -10,31 +10,29 @@ alabaster==1.0.0
# via sphinx
babel==2.17.0
# via sphinx
certifi==2025.4.26
certifi==2025.10.5
# via requests
charset-normalizer==3.4.2
charset-normalizer==3.4.3
# via requests
click==8.1.8
# via towncrier
docutils==0.21.2
# via sphinx
idna==3.4
idna==3.10
# via requests
imagesize==1.4.1
# via sphinx
incremental==24.7.2
# via towncrier
jinja2==3.1.6
# via
# sphinx
# towncrier
markupsafe==3.0.2
markupsafe==3.0.3
# via jinja2
packaging==25.0
# via sphinx
pygments==2.19.1
pygments==2.19.2
# via sphinx
requests==2.32.3
requests==2.32.5
# via sphinx
snowballstemmer==3.0.1
# via sphinx
@@ -58,16 +56,11 @@ sphinxcontrib-towncrier==0.5.0a0
# via -r requirements/doc.in
tomli==2.2.1
# via
# incremental
# sphinx
# towncrier
towncrier==23.11.0
towncrier==25.8.0
# via
# -r requirements/doc.in
# sphinxcontrib-towncrier
urllib3==2.4.0
urllib3==2.5.0
# via requests
# The following packages are considered to be unsafe in a requirements file:
setuptools==80.8.0
# via incremental
+1
View File
@@ -1,4 +1,5 @@
aiodns
backports.zstd; implementation_name == "cpython"
blockbuster
freezegun
isal
+36 -29
View File
@@ -1,18 +1,18 @@
#
# This file is autogenerated by pip-compile with python 3.10
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/lint.txt --resolver=backtracking --strip-extras requirements/lint.in
#
aiodns==3.4.0
aiodns==3.5.0
# via -r requirements/lint.in
annotated-types==0.7.0
# via pydantic
async-timeout==5.0.1
# via valkey
blockbuster==1.5.24
blockbuster==1.5.26
# via -r requirements/lint.in
cffi==1.17.1
cffi==2.0.0
# via
# cryptography
# pycares
@@ -21,19 +21,19 @@ cfgv==3.4.0
# via pre-commit
click==8.1.8
# via slotscheck
cryptography==45.0.2
cryptography==46.0.2
# via trustme
distlib==0.3.9
distlib==0.4.0
# via virtualenv
exceptiongroup==1.3.0
# via pytest
filelock==3.18.0
filelock==3.19.1
# via virtualenv
forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.1
freezegun==1.5.5
# via -r requirements/lint.in
identify==2.6.10
identify==2.6.15
# via pre-commit
idna==3.7
# via trustme
@@ -45,7 +45,7 @@ markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
mypy==1.15.0 ; implementation_name == "cpython"
mypy==1.18.2 ; implementation_name == "cpython"
# via -r requirements/lint.in
mypy-extensions==1.1.0
# via mypy
@@ -53,38 +53,42 @@ nodeenv==1.9.1
# via pre-commit
packaging==25.0
# via pytest
platformdirs==4.3.8
pathspec==0.12.1
# via mypy
platformdirs==4.4.0
# via virtualenv
pluggy==1.6.0
# via pytest
pre-commit==4.2.0
pre-commit==4.3.0
# via -r requirements/lint.in
pycares==4.8.0
pycares==4.11.0
# via aiodns
pycparser==2.22
pycparser==2.23
# via cffi
pydantic==2.11.5
pydantic==2.11.9
# via python-on-whales
pydantic-core==2.33.2
# via pydantic
pygments==2.19.1
# via rich
pytest==8.3.5
pygments==2.19.2
# via
# pytest
# rich
pytest==8.4.2
# via
# -r requirements/lint.in
# pytest-codspeed
# pytest-mock
pytest-codspeed==3.2.0
pytest-codspeed==4.0.0
# via -r requirements/lint.in
pytest-mock==3.14.0
pytest-mock==3.15.1
# via -r requirements/lint.in
python-dateutil==2.9.0.post0
# via freezegun
python-on-whales==0.76.1
python-on-whales==0.78.0
# via -r requirements/lint.in
pyyaml==6.0.2
pyyaml==6.0.3
# via pre-commit
rich==14.0.0
rich==14.1.0
# via pytest-codspeed
six==1.17.0
# via python-dateutil
@@ -97,22 +101,25 @@ tomli==2.2.1
# slotscheck
trustme==1.2.1
# via -r requirements/lint.in
typing-extensions==4.13.2
typing-extensions==4.15.0
# via
# cryptography
# exceptiongroup
# mypy
# pydantic
# pydantic-core
# python-on-whales
# rich
# typing-inspection
typing-inspection==0.4.1
# virtualenv
typing-inspection==0.4.2
# via pydantic
uvloop==0.21.0 ; platform_system != "Windows"
# via -r requirements/lint.in
valkey==6.1.0
valkey==6.1.1
# via -r requirements/lint.in
virtualenv==20.31.2
virtualenv==20.34.0
# via pre-commit
zlib-ng==0.5.1
zlib-ng==1.0.0
# via -r requirements/lint.in
backports.zstd==0.5.0 ; implementation_name == "cpython"
# via -r requirements/lint.in
+2 -2
View File
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/multidict.txt --resolver=backtracking --strip-extras requirements/multidict.in
#
multidict==6.4.4
multidict==6.6.4
# via -r requirements/multidict.in
typing-extensions==4.13.2
typing-extensions==4.15.0
# via multidict
+6 -5
View File
@@ -1,12 +1,13 @@
# Extracted from `setup.cfg` via `make sync-direct-runtime-deps`
# Extracted from `pyproject.toml` via `make sync-direct-runtime-deps`
aiodns >= 3.3.0
aiohappyeyeballs >= 2.5.0
aiosignal >= 1.1.2
async-timeout >= 4.0, < 6.0 ; python_version < "3.11"
aiosignal >= 1.4.0
async-timeout >= 4.0, < 6.0 ; python_version < '3.11'
attrs >= 17.3.0
Brotli; platform_python_implementation == 'CPython'
brotlicffi; platform_python_implementation != 'CPython'
backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14'
Brotli >= 1.2; platform_python_implementation == 'CPython'
brotlicffi >= 1.2; platform_python_implementation != 'CPython'
frozenlist >= 1.1.1
multidict >=4.5, < 7.0
propcache >= 0.2.0
+18 -14
View File
@@ -1,42 +1,46 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --strip-extras requirements/runtime-deps.in
#
aiodns==3.4.0
aiodns==3.5.0
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
aiosignal==1.3.2
aiosignal==1.4.0
# via -r requirements/runtime-deps.in
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
attrs==25.3.0
# via -r requirements/runtime-deps.in
brotli==1.1.0 ; platform_python_implementation == "CPython"
brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
cffi==1.17.1
cffi==2.0.0
# via pycares
frozenlist==1.6.0
frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
idna==3.4
idna==3.10
# via yarl
multidict==6.4.4
multidict==6.6.4
# via
# -r requirements/runtime-deps.in
# yarl
propcache==0.3.1
propcache==0.4.0
# via
# -r requirements/runtime-deps.in
# yarl
pycares==4.8.0
pycares==4.11.0
# via aiodns
pycparser==2.22
pycparser==2.23
# via cffi
typing-extensions==4.13.2
# via multidict
yarl==1.20.0
typing-extensions==4.15.0
# via
# aiosignal
# multidict
yarl==1.21.0
# via -r requirements/runtime-deps.in
backports.zstd==0.5.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
# via -r requirements/runtime-deps.in
@@ -1,16 +1,22 @@
#!/usr/bin/env python
"""Sync direct runtime dependencies from setup.cfg to runtime-deps.in."""
"""Sync direct runtime dependencies from pyproject.toml to runtime-deps.in."""
from configparser import ConfigParser
import sys
from pathlib import Path
cfg = ConfigParser()
cfg.read(Path("setup.cfg"))
reqs = cfg["options"]["install_requires"] + cfg.items("options.extras_require")[0][1]
reqs = sorted(reqs.split("\n"), key=str.casefold)
reqs.remove("")
if sys.version_info >= (3, 11):
import tomllib
else:
raise RuntimeError("Use Python 3.11+ to run 'make sync-direct-runtime-deps'")
data = tomllib.loads(Path("pyproject.toml").read_text())
reqs = (
data["project"]["dependencies"]
+ data["project"]["optional-dependencies"]["speedups"]
)
reqs = sorted(reqs, key=str.casefold)
with open(Path("requirements", "runtime-deps.in"), "w") as outfile:
header = "# Extracted from `setup.cfg` via `make sync-direct-runtime-deps`\n\n"
header = "# Extracted from `pyproject.toml` via `make sync-direct-runtime-deps`\n\n"
outfile.write(header)
outfile.write("\n".join(reqs) + "\n")
+18
View File
@@ -0,0 +1,18 @@
blockbuster
coverage
freezegun
isal; python_version < "3.14" # no wheel for 3.14
mypy; implementation_name == "cpython"
pkgconfig
proxy.py >= 2.4.4rc5
pytest
pytest-cov
pytest-mock
pytest-xdist
pytest_codspeed
python-on-whales
re-assert
setuptools-git
trustme; platform_machine != "i686" # no 32-bit wheels
wait-for-it
zlib_ng
+117
View File
@@ -0,0 +1,117 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/test-common.txt --strip-extras requirements/test-common.in
#
annotated-types==0.7.0
# via pydantic
blockbuster==1.5.26
# via -r requirements/test-common.in
cffi==2.0.0
# via
# cryptography
# pytest-codspeed
click==8.2.1
# via wait-for-it
coverage==7.10.7
# via
# -r requirements/test-common.in
# pytest-cov
cryptography==46.0.2
# via trustme
exceptiongroup==1.3.0
# via pytest
execnet==2.1.1
# via pytest-xdist
forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.5
# via -r requirements/test-common.in
idna==3.10
# via trustme
iniconfig==2.1.0
# via pytest
isal==1.8.0 ; python_version < "3.14"
# via -r requirements/test-common.in
markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
mypy==1.18.2 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
packaging==25.0
# via pytest
pathspec==0.12.1
# via mypy
pkgconfig==1.5.5
# via -r requirements/test-common.in
pluggy==1.6.0
# via
# pytest
# pytest-cov
proxy-py==2.4.10
# via -r requirements/test-common.in
pycparser==2.23
# via cffi
pydantic==2.12.0a1
# via python-on-whales
pydantic-core==2.37.2
# via pydantic
pygments==2.19.2
# via
# pytest
# rich
pytest==8.4.2
# via
# -r requirements/test-common.in
# pytest-codspeed
# pytest-cov
# pytest-mock
# pytest-xdist
pytest-codspeed==4.0.0
# via -r requirements/test-common.in
pytest-cov==7.0.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
python-on-whales==0.78.0
# via -r requirements/test-common.in
re-assert==1.1.0
# via -r requirements/test-common.in
regex==2025.9.18
# via re-assert
rich==14.1.0
# via pytest-codspeed
setuptools-git==1.2
# via -r requirements/test-common.in
six==1.17.0
# via python-dateutil
tomli==2.2.1
# via
# coverage
# mypy
# pytest
trustme==1.2.1 ; platform_machine != "i686"
# via -r requirements/test-common.in
typing-extensions==4.15.0
# via
# cryptography
# exceptiongroup
# mypy
# pydantic
# pydantic-core
# python-on-whales
# typing-inspection
typing-inspection==0.4.2
# via pydantic
wait-for-it==2.3.0
# via -r requirements/test-common.in
zlib-ng==1.0.0
# via -r requirements/test-common.in
+2
View File
@@ -0,0 +1,2 @@
-r base-ft.in
-r test-common.in
+156
View File
@@ -0,0 +1,156 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/test-ft.txt --strip-extras requirements/test-ft.in
#
aiodns==3.5.0
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
aiosignal==1.4.0
# via -r requirements/runtime-deps.in
annotated-types==0.7.0
# via pydantic
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
attrs==25.3.0
# via -r requirements/runtime-deps.in
blockbuster==1.5.26
# via -r requirements/test-common.in
brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
cffi==2.0.0
# via
# cryptography
# pycares
# pytest-codspeed
click==8.2.1
# via wait-for-it
coverage==7.10.7
# via
# -r requirements/test-common.in
# pytest-cov
cryptography==46.0.2
# via trustme
exceptiongroup==1.3.0
# via pytest
execnet==2.1.1
# via pytest-xdist
forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.5
# via -r requirements/test-common.in
frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
gunicorn==23.0.0
# via -r requirements/base-ft.in
idna==3.10
# via
# trustme
# yarl
iniconfig==2.1.0
# via pytest
isal==1.8.0 ; python_version < "3.14"
# via -r requirements/test-common.in
markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
multidict==6.6.4
# via
# -r requirements/runtime-deps.in
# yarl
mypy==1.18.2 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
packaging==25.0
# via
# gunicorn
# pytest
pathspec==0.12.1
# via mypy
pkgconfig==1.5.5
# via -r requirements/test-common.in
pluggy==1.6.0
# via
# pytest
# pytest-cov
propcache==0.4.0
# via
# -r requirements/runtime-deps.in
# yarl
proxy-py==2.4.10
# via -r requirements/test-common.in
pycares==4.11.0
# via aiodns
pycparser==2.23
# via cffi
pydantic==2.12.0a1
# via python-on-whales
pydantic-core==2.37.2
# via pydantic
pygments==2.19.2
# via
# pytest
# rich
pytest==8.4.2
# via
# -r requirements/test-common.in
# pytest-codspeed
# pytest-cov
# pytest-mock
# pytest-xdist
pytest-codspeed==4.0.0
# via -r requirements/test-common.in
pytest-cov==7.0.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
python-on-whales==0.78.0
# via -r requirements/test-common.in
re-assert==1.1.0
# via -r requirements/test-common.in
regex==2025.9.18
# via re-assert
rich==14.1.0
# via pytest-codspeed
setuptools-git==1.2
# via -r requirements/test-common.in
six==1.17.0
# via python-dateutil
tomli==2.2.1
# via
# coverage
# mypy
# pytest
trustme==1.2.1 ; platform_machine != "i686"
# via -r requirements/test-common.in
typing-extensions==4.15.0
# via
# aiosignal
# cryptography
# exceptiongroup
# multidict
# mypy
# pydantic
# pydantic-core
# python-on-whales
# typing-inspection
typing-inspection==0.4.2
# via pydantic
wait-for-it==2.3.0
# via -r requirements/test-common.in
yarl==1.21.0
# via -r requirements/runtime-deps.in
zlib-ng==1.0.0
# via -r requirements/test-common.in
backports.zstd==0.5.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
# via -r requirements/runtime-deps.in
+1 -19
View File
@@ -1,20 +1,2 @@
-r base.in
blockbuster
coverage
freezegun
isal
mypy; implementation_name == "cpython"
pkgconfig
proxy.py >= 2.4.4rc5
pytest
pytest-cov
pytest-mock
pytest-xdist
pytest_codspeed
python-on-whales
re-assert
setuptools-git
trustme; platform_machine != "i686" # no 32-bit wheels
wait-for-it
zlib_ng
-r test-common.in

Some files were not shown because too many files have changed in this diff Show More