Add an integration test for publishing to pyx (#16740)

This commit is contained in:
William Woodruff 2025-11-18 12:13:57 -05:00 committed by GitHub
parent 512c0ca5ed
commit f78ddf05c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 31 deletions

View File

@ -1991,6 +1991,7 @@ jobs:
UV_TEST_PUBLISH_GITLAB_PAT: ${{ secrets.UV_TEST_PUBLISH_GITLAB_PAT }} UV_TEST_PUBLISH_GITLAB_PAT: ${{ secrets.UV_TEST_PUBLISH_GITLAB_PAT }}
UV_TEST_PUBLISH_CODEBERG_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CODEBERG_TOKEN }} UV_TEST_PUBLISH_CODEBERG_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CODEBERG_TOKEN }}
UV_TEST_PUBLISH_CLOUDSMITH_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CLOUDSMITH_TOKEN }} UV_TEST_PUBLISH_CLOUDSMITH_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CLOUDSMITH_TOKEN }}
UV_TEST_PUBLISH_PYX_TOKEN: ${{ secrets.UV_TEST_PUBLISH_PYX_TOKEN }}
UV_TEST_PUBLISH_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} UV_TEST_PUBLISH_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
integration-uv-build-backend: integration-uv-build-backend:

View File

@ -58,6 +58,7 @@ Web: https://codeberg.org/astral-test-user/-/packages/pypi/astral-test-token/0.1
Docs: https://forgejo.org/docs/latest/user/packages/pypi/ Docs: https://forgejo.org/docs/latest/user/packages/pypi/
""" """
import logging
import os import os
import re import re
import shutil import shutil
@ -166,15 +167,30 @@ local_targets: dict[str, TargetConfiguration] = {
"https://python.cloudsmith.io/astral-test/astral-test-1/", "https://python.cloudsmith.io/astral-test/astral-test-1/",
"https://dl.cloudsmith.io/public/astral-test/astral-test-1/python/simple/", "https://dl.cloudsmith.io/public/astral-test/astral-test-1/python/simple/",
), ),
"pyx-token": TargetConfiguration(
"astral-test-token",
"https://astral-sh-staging-api.pyx.dev/v1/upload/uv-publish-integration/main",
"https://astral-sh-staging-api.pyx.dev/simple/uv-publish-integration/main/",
),
} }
all_targets: dict[str, TargetConfiguration] = local_targets | { all_targets: dict[str, TargetConfiguration] = local_targets | {
"pypi-trusted-publishing": TargetConfiguration( "pypi-trusted-publishing": TargetConfiguration(
"astral-test-trusted-publishing", "astral-test-trusted-publishing",
TEST_PYPI_PUBLISH_URL, TEST_PYPI_PUBLISH_URL,
"https://test.pypi.org/simple/", "https://test.pypi.org/simple/",
) ),
# TODO: Not enabled until we have a native Trusted Publishing flow for pyx in uv.
# "pyx-trusted-publishing": TargetConfiguration(
# "astral-test-trusted-publishing",
# "https://api.pyx.dev/v1/upload/astral-test/main",
# "https://api.pyx.dev/simple/astral-test/main",
# ),
} }
# Temporarily disable codeberg on CI due to unreliability.
all_targets.pop("codeberg", None)
def get_latest_version(target: str, client: httpx.Client) -> Version | None: def get_latest_version(target: str, client: httpx.Client) -> Version | None:
"""Return the latest version on all indexes of the package.""" """Return the latest version on all indexes of the package."""
@ -235,7 +251,8 @@ def collect_versions(url: str, client: httpx.Client) -> set[Version]:
def get_filenames(url: str, client: httpx.Client) -> list[str]: def get_filenames(url: str, client: httpx.Client) -> list[str]:
"""Get the filenames (source dists and wheels) from an index URL.""" """Get the filenames (source dists and wheels) from an index URL."""
response = client.get(url) response = client.get(url, follow_redirects=True)
response.raise_for_status()
data = response.text data = response.text
# Works for the indexes in the list # Works for the indexes in the list
href_text = r"<a(?:\s*[\w-]+=(?:'[^']+'|\"[^\"]+\"))* *>([^<>]+)</a>" href_text = r"<a(?:\s*[\w-]+=(?:'[^']+'|\"[^\"]+\"))* *>([^<>]+)</a>"
@ -305,6 +322,7 @@ def wait_for_index(
project_name: str, project_name: str,
version: Version, version: Version,
uv: Path, uv: Path,
env: dict,
): ):
"""Check that the index URL was updated, wait up to 100s if necessary. """Check that the index URL was updated, wait up to 100s if necessary.
@ -333,6 +351,7 @@ def wait_for_index(
text=True, text=True,
input=f"{project_name}", input=f"{project_name}",
stdout=PIPE, stdout=PIPE,
env=env,
) )
# codeberg sometimes times out # codeberg sometimes times out
if result.returncode != 0: if result.returncode != 0:
@ -368,6 +387,13 @@ def publish_project(target: str, uv: Path, client: httpx.Client):
2. If we're using PyPI, uploading the same files again succeeds. 2. If we're using PyPI, uploading the same files again succeeds.
3. Check URL works and reports the files as skipped. 3. Check URL works and reports the files as skipped.
""" """
# If we're publishing to pyx, we need to give the httpx client
# access to an appropriate credential.
if target == "pyx-token":
client.auth = httpx.BasicAuth(
username="__token__", password=os.environ["UV_TEST_PUBLISH_PYX_TOKEN"]
)
project_name = all_targets[target].project_name project_name = all_targets[target].project_name
# If a version was recently uploaded by another run of this script, # If a version was recently uploaded by another run of this script,
@ -423,7 +449,7 @@ def publish_project(target: str, uv: Path, client: httpx.Client):
f"\n=== 2. Publishing {project_name} {version} again (PyPI) ===", f"\n=== 2. Publishing {project_name} {version} again (PyPI) ===",
file=sys.stderr, file=sys.stderr,
) )
wait_for_index(index_url, project_name, version, uv) wait_for_index(index_url, project_name, version, uv, env)
args = [uv, "publish", "--publish-url", publish_url, *extra_args] args = [uv, "publish", "--publish-url", publish_url, *extra_args]
output = run( output = run(
args, cwd=project_dir, env=env, text=True, check=True, stderr=PIPE args, cwd=project_dir, env=env, text=True, check=True, stderr=PIPE
@ -444,7 +470,7 @@ def publish_project(target: str, uv: Path, client: httpx.Client):
f"\n=== 3. Publishing {project_name} {version} again with {mode} ===", f"\n=== 3. Publishing {project_name} {version} again with {mode} ===",
file=sys.stderr, file=sys.stderr,
) )
wait_for_index(index_url, project_name, version, uv) wait_for_index(index_url, project_name, version, uv, env)
# Test twine-style and index-style uploads for different packages. # Test twine-style and index-style uploads for different packages.
if index := all_targets[target].index: if index := all_targets[target].index:
args = [ args = [
@ -487,7 +513,7 @@ def publish_project(target: str, uv: Path, client: httpx.Client):
f"again with skip existing (error test) ===", f"again with skip existing (error test) ===",
file=sys.stderr, file=sys.stderr,
) )
wait_for_index(index_url, project_name, version, uv) wait_for_index(index_url, project_name, version, uv, env)
args = [ args = [
uv, uv,
"publish", "publish",
@ -540,12 +566,25 @@ def target_configuration(target: str) -> tuple[dict[str, str], list[str]]:
env = { env = {
"UV_PUBLISH_TOKEN": os.environ["UV_TEST_PUBLISH_CLOUDSMITH_TOKEN"], "UV_PUBLISH_TOKEN": os.environ["UV_TEST_PUBLISH_CLOUDSMITH_TOKEN"],
} }
elif target == "pyx-token":
extra_args = []
env = {
"UV_API_KEY": os.environ["UV_TEST_PUBLISH_PYX_TOKEN"],
# So that uv accesses the right API for check-url.
"PYX_API_URL": "https://astral-sh-staging-api.pyx.dev",
}
else: else:
raise ValueError(f"Unknown target: {target}") raise ValueError(f"Unknown target: {target}")
return env, extra_args return env, extra_args
def main(): def main():
logging.basicConfig(
format="%(levelname)s [%(asctime)s] %(name)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
level=logging.DEBUG,
)
parser = ArgumentParser() parser = ArgumentParser()
target_choices = [*all_targets, "local", "all"] target_choices = [*all_targets, "local", "all"]
parser.add_argument("targets", choices=target_choices, nargs="+") parser.add_argument("targets", choices=target_choices, nargs="+")
@ -568,8 +607,10 @@ def main():
else: else:
targets = args.targets targets = args.targets
with httpx.Client(timeout=120) as client:
for project_name in targets: for project_name in targets:
# Each publish gets its own client, since we may need to introduce
# target-specific authentication.
with httpx.Client(timeout=120) as client:
publish_project(project_name, uv, client) publish_project(project_name, uv, client)

View File

@ -1,4 +1,5 @@
version = 1 version = 1
revision = 3
requires-python = ">=3.12" requires-python = ">=3.12"
[manifest] [manifest]
@ -9,47 +10,47 @@ requirements = [
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.8.0" version = "4.11.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "idna" }, { name = "idna" },
{ name = "sniffio" }, { name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
] ]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2024.12.14" version = "2025.11.12"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
] ]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.14.0" version = "0.16.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
] ]
[[package]] [[package]]
name = "httpcore" name = "httpcore"
version = "1.0.7" version = "1.0.9"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
{ name = "h11" }, { name = "h11" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
] ]
[[package]] [[package]]
@ -62,43 +63,43 @@ dependencies = [
{ name = "httpcore" }, { name = "httpcore" },
{ name = "idna" }, { name = "idna" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
] ]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.11"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
] ]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "24.2" version = "24.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" },
] ]
[[package]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.1" version = "1.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
] ]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.15.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
] ]