This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git
The following commit(s) were added to refs/heads/main by this push:
new 629b32c Migrate httpx to aiohttp, and remove httpx
629b32c is described below
commit 629b32c0417b3a64b98fb528edd5ce3d7e2b190a
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Jun 26 19:59:46 2025 +0100
Migrate httpx to aiohttp, and remove httpx
---
atr/blueprints/admin/admin.py | 4 ++--
atr/datasources/apache.py | 50 +++++++++++++++++++++----------------------
atr/routes/keys.py | 16 +++++++-------
atr/routes/vote.py | 12 +++++------
atr/util.py | 39 ++++++++++++++++-----------------
poetry.lock | 49 +-----------------------------------------
pyproject.toml | 1 -
uv.lock | 38 +++++++-------------------------
8 files changed, 69 insertions(+), 140 deletions(-)
diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 3322f3a..c4ee69f 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -27,11 +27,11 @@ from collections.abc import Callable, Mapping
from typing import Any, Final
import aiofiles.os
+import aiohttp
import aioshutil
import asfquart
import asfquart.base as base
import asfquart.session as session
-import httpx
import quart
import sqlalchemy.orm as orm
import sqlmodel
@@ -577,7 +577,7 @@ async def admin_projects_update() -> str |
response.Response | tuple[Mapping[str
f"(PMCs and PPMCs) with membership data",
"category": "success",
}, 200
- except httpx.RequestError as e:
+ except aiohttp.ClientError as e:
return {
"message": f"Failed to fetch data: {e!s}",
"category": "error",
diff --git a/atr/datasources/apache.py b/atr/datasources/apache.py
index daa87e1..c3b2da1 100644
--- a/atr/datasources/apache.py
+++ b/atr/datasources/apache.py
@@ -22,7 +22,7 @@ from __future__ import annotations
import datetime
from typing import Annotated, Any, Final
-import httpx
+import aiohttp
import atr.schema as schema
@@ -215,10 +215,10 @@ class ProjectsData(schema.DictRoot[ProjectStatus]):
async def get_active_committee_data() -> CommitteeData:
"""Returns the list of currently active committees."""
- async with httpx.AsyncClient() as client:
- response = await client.get(_WHIMSY_COMMITTEE_INFO_URL)
- response.raise_for_status()
- data = response.json()
+ async with aiohttp.ClientSession() as session:
+ async with session.get(_WHIMSY_COMMITTEE_INFO_URL) as response:
+ response.raise_for_status()
+ data = await response.json()
return CommitteeData.model_validate(data)
@@ -226,28 +226,28 @@ async def get_active_committee_data() -> CommitteeData:
async def get_current_podlings_data() -> PodlingsData:
"""Returns the list of current podlings."""
- async with httpx.AsyncClient() as client:
- response = await client.get(_PROJECTS_PODLINGS_URL)
- response.raise_for_status()
- data = response.json()
+ async with aiohttp.ClientSession() as session:
+ async with session.get(_PROJECTS_PODLINGS_URL) as response:
+ response.raise_for_status()
+ data = await response.json()
return PodlingsData.model_validate(data)
async def get_groups_data() -> GroupsData:
"""Returns LDAP Groups with their members."""
- async with httpx.AsyncClient() as client:
- response = await client.get(_PROJECTS_GROUPS_URL)
- response.raise_for_status()
- data = response.json()
+ async with aiohttp.ClientSession() as session:
+ async with session.get(_PROJECTS_GROUPS_URL) as response:
+ response.raise_for_status()
+ data = await response.json()
return GroupsData.model_validate(data)
async def get_ldap_projects_data() -> LDAPProjectsData:
- async with httpx.AsyncClient() as client:
- response = await client.get(_WHIMSY_PROJECTS_URL)
- response.raise_for_status()
- data = response.json()
+ async with aiohttp.ClientSession() as session:
+ async with session.get(_WHIMSY_PROJECTS_URL) as response:
+ response.raise_for_status()
+ data = await response.json()
return LDAPProjectsData.model_validate(data)
@@ -255,19 +255,19 @@ async def get_ldap_projects_data() -> LDAPProjectsData:
async def get_projects_data() -> ProjectsData:
"""Returns the list of projects."""
- async with httpx.AsyncClient() as client:
- response = await client.get(_PROJECTS_PROJECTS_URL)
- response.raise_for_status()
- data = response.json()
+ async with aiohttp.ClientSession() as session:
+ async with session.get(_PROJECTS_PROJECTS_URL) as response:
+ response.raise_for_status()
+ data = await response.json()
return ProjectsData.model_validate(data)
async def get_retired_committee_data() -> RetiredCommitteeData:
"""Returns the list of retired committees."""
- async with httpx.AsyncClient() as client:
- response = await client.get(_WHIMSY_COMMITTEE_RETIRED_URL)
- response.raise_for_status()
- data = response.json()
+ async with aiohttp.ClientSession() as session:
+ async with session.get(_WHIMSY_COMMITTEE_RETIRED_URL) as response:
+ response.raise_for_status()
+ data = await response.json()
return RetiredCommitteeData.model_validate(data)
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index da2dbe9..c875f86 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -30,9 +30,9 @@ import textwrap
from collections.abc import Awaitable, Callable, Sequence
import aiofiles.os
+import aiohttp
import asfquart as asfquart
import asfquart.base as base
-import httpx
import quart
import werkzeug.datastructures as datastructures
import werkzeug.wrappers.response as response
@@ -636,13 +636,13 @@ async def _format_keys_file(
async def _get_keys_text(keys_url: str, render: Callable[[str],
Awaitable[str]]) -> str:
try:
- async with httpx.AsyncClient() as client:
- response = await client.get(keys_url, follow_redirects=True)
- response.raise_for_status()
- return response.text
- except httpx.HTTPStatusError as e:
- raise base.ASFQuartException(f"Error fetching URL:
{e.response.status_code} {e.response.reason_phrase}")
- except httpx.RequestError as e:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(keys_url, allow_redirects=True) as response:
+ response.raise_for_status()
+ return await response.text()
+ except aiohttp.ClientResponseError as e:
+ raise base.ASFQuartException(f"Error fetching URL: {e.status}
{e.message}")
+ except aiohttp.ClientError as e:
raise base.ASFQuartException(f"Error fetching URL: {e}")
diff --git a/atr/routes/vote.py b/atr/routes/vote.py
index e90ffac..f23fc15 100644
--- a/atr/routes/vote.py
+++ b/atr/routes/vote.py
@@ -19,7 +19,7 @@ import json
import logging
import os
-import httpx
+import aiohttp
import quart
import werkzeug.wrappers.response as response
import wtforms
@@ -164,11 +164,11 @@ async def _task_archive_url(task_mid: str) -> str | None:
lid = "user-tests.tooling.apache.org"
url =
f"https://lists.apache.org/api/email.lua?id=%3C{task_mid}%3E&listid=%3C{lid}%3E"
try:
- async with httpx.AsyncClient() as client:
- response = await client.get(url)
- response.raise_for_status()
- # TODO: Check whether this blocks from network
- email_data = response.json()
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url) as response:
+ response.raise_for_status()
+ # TODO: Check whether this blocks from network
+ email_data = await response.json()
mid = email_data["mid"]
if not isinstance(mid, str):
return None
diff --git a/atr/util.py b/atr/util.py
index 0aa3549..e554ea2 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -33,12 +33,12 @@ from collections.abc import AsyncGenerator, Callable,
Iterable, Sequence
from typing import Any, Final, TypeVar
import aiofiles.os
+import aiohttp
import aioshutil
import asfquart
import asfquart.base as base
import asfquart.session as session
import gitignore_parser
-import httpx
import jinja2
import quart
import quart_wtf
@@ -463,26 +463,25 @@ def get_unfinished_dir() -> pathlib.Path:
async def get_urls_as_completed(urls: Sequence[str]) ->
AsyncGenerator[tuple[str, int | str | None, bytes]]:
"""GET a list of URLs in parallel and yield (url, status, content_bytes)
as they become available."""
- async with httpx.AsyncClient() as client:
- tasks = [asyncio.create_task(client.get(url)) for url in urls]
- for future in asyncio.as_completed(tasks):
- try:
- response = await future
- except Exception as e:
- yield ("", str(e), b"")
- continue
- url = str(response.url)
+ async with aiohttp.ClientSession() as session:
+
+ async def _fetch(one_url: str) -> tuple[str, int | str | None, bytes]:
try:
- response.raise_for_status()
- yield (url, response.status_code, await response.aread())
- except httpx.HTTPStatusError as e:
- if e.response.status_code == 200:
- # This should not happen
- yield (url, str(e), b"")
- else:
- yield (url, e.response.status_code, b"")
- except Exception as e:
- yield (url, str(e), b"")
+ async with session.get(one_url) as resp:
+ try:
+ resp.raise_for_status()
+ return (str(resp.url), resp.status, await resp.read())
+ except aiohttp.ClientResponseError as e:
+ url = str(e.request_info.real_url)
+ if e.status == 200:
+ return (url, str(e), b"")
+ return (url, e.status, b"")
+ except Exception as exc:
+ return ("", str(exc), b"")
+
+ tasks = [asyncio.create_task(_fetch(u)) for u in urls]
+ for future in asyncio.as_completed(tasks):
+ yield await future
async def has_files(release: models.Release) -> bool:
diff --git a/poetry.lock b/poetry.lock
index e217940..3588a71 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1164,53 +1164,6 @@ files = [
{file = "hpack-4.1.0.tar.gz", hash =
"sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"},
]
-[[package]]
-name = "httpcore"
-version = "1.0.9"
-description = "A minimal low-level HTTP client."
-optional = false
-python-versions = ">=3.8"
-groups = ["main"]
-files = [
- {file = "httpcore-1.0.9-py3-none-any.whl", hash =
"sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
- {file = "httpcore-1.0.9.tar.gz", hash =
"sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
-]
-
-[package.dependencies]
-certifi = "*"
-h11 = ">=0.16"
-
-[package.extras]
-asyncio = ["anyio (>=4.0,<5.0)"]
-http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (==1.*)"]
-trio = ["trio (>=0.22.0,<1.0)"]
-
-[[package]]
-name = "httpx"
-version = "0.28.1"
-description = "The next generation HTTP client."
-optional = false
-python-versions = ">=3.8"
-groups = ["main"]
-files = [
- {file = "httpx-0.28.1-py3-none-any.whl", hash =
"sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
- {file = "httpx-0.28.1.tar.gz", hash =
"sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
-]
-
-[package.dependencies]
-anyio = "*"
-certifi = "*"
-httpcore = "==1.*"
-idna = "*"
-
-[package.extras]
-brotli = ["brotli ; platform_python_implementation == \"CPython\"",
"brotlicffi ; platform_python_implementation != \"CPython\""]
-cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
-http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (==1.*)"]
-zstd = ["zstandard (>=0.18.0)"]
-
[[package]]
name = "hypercorn"
version = "0.17.3"
@@ -3195,4 +3148,4 @@ propcache = ">=0.2.1"
[metadata]
lock-version = "2.1"
python-versions = "~=3.13"
-content-hash =
"54eafdba2809654d5c799509841e7fb6e3cb67635d88f7770de3217dc5c68c44"
+content-hash =
"3cf10d16cd21f3d39e21f7cb26cfbede7ecb0da0a0b893b9a49fb7b73e2aab3a"
diff --git a/pyproject.toml b/pyproject.toml
index 2153c78..2b1e1d8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -27,7 +27,6 @@ dependencies = [
"email-validator~=2.2.0",
"gitignore-parser (>=0.1.12,<0.2.0)",
"greenlet>=3.1.1,<4.0.0",
- "httpx~=0.27",
"hypercorn~=0.17",
"ldap3 (==2.10.2rc2)",
"python-decouple~=3.8",
diff --git a/uv.lock b/uv.lock
index 6b3b278..cc93d65 100644
--- a/uv.lock
+++ b/uv.lock
@@ -531,6 +531,12 @@ wheels = [
{ url =
"https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl",
hash =
"sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size
= 13106 },
]
+[[package]]
+name = "gitignore-parser"
+version = "0.1.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url =
"https://files.pythonhosted.org/packages/86/a8/faf07759672973362e3f1f9742281a90aec7846e8a4043c4df5652990054/gitignore_parser-0.1.12.tar.gz",
hash =
"sha256:78b22243adc0f02102c56c5e8c9a1d9121394142ca6143a90daa7f8d7a07a17e", size
= 5407 }
+
[[package]]
name = "greenlet"
version = "3.2.3"
@@ -586,34 +592,6 @@ wheels = [
{ url =
"https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl",
hash =
"sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size
= 34357 },
]
-[[package]]
-name = "httpcore"
-version = "1.0.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "h11" },
-]
-sdist = { url =
"https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz",
hash =
"sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size
= 85484 }
-wheels = [
- { url =
"https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl",
hash =
"sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size
= 78784 },
-]
-
-[[package]]
-name = "httpx"
-version = "0.28.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "certifi" },
- { name = "httpcore" },
- { name = "idna" },
-]
-sdist = { url =
"https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz",
hash =
"sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size
= 141406 }
-wheels = [
- { url =
"https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl",
hash =
"sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size
= 73517 },
-]
-
[[package]]
name = "hypercorn"
version = "0.17.3"
@@ -1401,8 +1379,8 @@ dependencies = [
{ name = "dnspython" },
{ name = "dunamai" },
{ name = "email-validator" },
+ { name = "gitignore-parser" },
{ name = "greenlet" },
- { name = "httpx" },
{ name = "hypercorn" },
{ name = "ldap3" },
{ name = "python-decouple" },
@@ -1447,8 +1425,8 @@ requires-dist = [
{ name = "dnspython", specifier = ">=2.7.0,<3.0.0" },
{ name = "dunamai", specifier = ">=1.23.0" },
{ name = "email-validator", specifier = "~=2.2.0" },
+ { name = "gitignore-parser", specifier = ">=0.1.12,<0.2.0" },
{ name = "greenlet", specifier = ">=3.1.1,<4.0.0" },
- { name = "httpx", specifier = "~=0.27" },
{ name = "hypercorn", specifier = "~=0.17" },
{ name = "ldap3", specifier = "==2.10.2rc2" },
{ name = "python-decouple", specifier = "~=3.8" },
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]