From 9c4d81a3f02c8a6017eec2df8ae56295e537e934 Mon Sep 17 00:00:00 2001 From: Ali Khokhar Date: Fri, 25 Sep 2026 14:21:55 -0700 Subject: [PATCH 1/4] patch: Automate release versions and GitHub Releases (#1903) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Manual version bumps create conflicts between PRs and require contributors to update package metadata before the final release version is known. Release intent should be declared in the PR title and applied once after merge. ## How Require exactly `patch: `, `minor: `, or `major: ` for changes to assets, scripts, source, Python version, pyproject.toml, or uv.lock. Forbid release prefixes when none of those paths change. Keep the required Version policy check and rerun it independently on title edits. Use hatch-vcs and Git release tags instead of maintained version numbers. Installed FCC keeps reading its distribution metadata. Git-aware uv cache keys refresh local development installs without changing the lockfile. Post-merge reads the saved squash title, reserves one version at that commit, stages validated distributions on a draft GitHub Release, publishes the same files to PyPI, then publishes the Release with generated notes. Retries reuse the version and staged bytes. Later releases stop until an unfinished predecessor completes. Recovery dispatch can retry an earlier main commit using the repaired workflow. Update the issue-version bot to compare against the latest published Release and document contributor title rules. Establish the existing, successfully published 6.2.66 version as the initial release tag. The accepted contract retains the existing release paths, ordered queue, trusted PyPI publisher, and ordinary pull-request check trust model. There are no manual version bumps or release-only commits.

RetriggerConfidence Score: 4/5

Not safe to merge until the outstanding release-finalization issue is fixed.

Fix All in CodexFindings

1. P1 **Conflicting fork tags fail CI** ▶ 2. P1 **Release finalization always fails** ▶
Summary The PR automates release versioning and publication. The fork-tag CI conflict is fixed, but release finalization remains broken.
Reviews (3) · Last reviewed commit: ["Use canonical release tags in disposable..."](https://github.com/alishahryar1/free-claude-code/commit/fc416b266774a74a8879432d01527b3255b3c922) --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- .github/actions/ci-environment/action.yml | 7 + .github/workflows/post-merge.yml | 58 +--- .github/workflows/tests.yml | 39 +-- .../workflows/validate-bug-report-version.yml | 56 ++-- .github/workflows/version-policy.yml | 51 ++++ CONTRIBUTING.md | 10 +- pyproject.toml | 8 +- scripts/check_version_policy.py | 183 ++++++------ scripts/publish_release.py | 207 ++++++++++++++ scripts/validate_release.py | 14 +- .../test_issue_form_version_validation.py | 152 ++++++---- tests/contracts/test_uv_policy.py | 1 + tests/core/test_version.py | 9 - tests/scripts/test_publish_release.py | 264 ++++++++++++++++++ tests/scripts/test_release_package.py | 4 +- tests/scripts/test_release_workflow.py | 165 ++++++----- tests/scripts/test_version_policy.py | 251 ++++++++--------- uv.lock | 1 - 19 files changed, 993 insertions(+), 489 deletions(-) create mode 100644 .github/workflows/version-policy.yml create mode 100644 scripts/publish_release.py create mode 100644 tests/scripts/test_publish_release.py diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 5c89155667..6b4d965637 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -7,7 +7,7 @@ body: id: fcc-version attributes: label: FCC version - description: Run `fcc-server --version` and include one version in `number.number.number` format. If FCC did not install, enter `None`. + description: Run `fcc-server --version` and include one version exactly as printed, including any development suffix. If FCC did not install, enter `None`. placeholder: "The version is 1.22.333, or None" validations: required: true diff --git a/.github/actions/ci-environment/action.yml b/.github/actions/ci-environment/action.yml index b8a330c812..438865ddd7 100644 --- a/.github/actions/ci-environment/action.yml +++ b/.github/actions/ci-environment/action.yml @@ -44,6 +44,13 @@ outputs: runs: using: composite steps: + - name: Fetch canonical release tags + shell: bash + env: + CI_SERVER: ${{ github.server_url }} + CI_REPOSITORY: ${{ github.repository }} + run: git fetch --prune --no-tags "$CI_SERVER/$CI_REPOSITORY.git" '+refs/tags/*:refs/tags/*' + - name: Prepare cache identity id: identity shell: bash diff --git a/.github/workflows/post-merge.yml b/.github/workflows/post-merge.yml index 55ea434354..3d99fe81d3 100644 --- a/.github/workflows/post-merge.yml +++ b/.github/workflows/post-merge.yml @@ -7,6 +7,11 @@ env: on: workflow_dispatch: + inputs: + release_commit: + description: Full main commit SHA to retry (defaults to this workflow commit) + required: false + type: string push: branches: [main] @@ -37,7 +42,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: ref: ${{ github.sha }} - fetch-depth: 1 + fetch-depth: 0 - name: Set up CI environment id: environment @@ -81,21 +86,13 @@ jobs: timeout-minutes: 15 environment: pypi permissions: - contents: read + contents: write id-token: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: ref: ${{ github.sha }} - fetch-depth: 2 - - - name: Fetch previous revision - env: - BEFORE_SHA: ${{ github.event.before }} - run: | - if [ -n "$BEFORE_SHA" ]; then - git fetch --no-tags --depth=1 origin "$BEFORE_SHA" - fi + fetch-depth: 0 - name: Set up CI environment uses: ./.github/actions/ci-environment @@ -105,38 +102,11 @@ jobs: lookup-only: "true" sync-dependencies: "false" - - name: Detect release - id: release + - name: Publish release env: - BEFORE_SHA: ${{ github.event.before }} - HEAD_SHA: ${{ github.sha }} + GH_TOKEN: ${{ github.token }} + RELEASE_REPOSITORY: ${{ github.repository }} + RELEASE_COMMIT: ${{ inputs.release_commit || github.sha }} run: | - uv run --no-project --python "$(cat .python-version)" python - <<'PY' - import os - import subprocess - import tomllib - from pathlib import Path - - head = os.environ["HEAD_SHA"] - before = os.environ.get("BEFORE_SHA") or f"{head}^" - def version(revision): - result = subprocess.run( - ["git", "show", f"{revision}:pyproject.toml"], - check=True, capture_output=True, text=True, - ) - return tomllib.loads(result.stdout)["project"]["version"] - current, previous = version(head), version(before) - with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output: - output.write(f"changed={str(current != previous).lower()}\n") - print(f"Release version: {previous} -> {current}") - PY - - - name: Build and validate release - if: steps.release.outputs.changed == 'true' - run: | - uv build --no-sources --out-dir "$RUNNER_TEMP/fcc-release" - uv run --no-project --python "$(cat .python-version)" python scripts/validate_release.py "$RUNNER_TEMP/fcc-release" - - - name: Publish release - if: steps.release.outputs.changed == 'true' - run: uv publish --trusted-publishing always "$RUNNER_TEMP/fcc-release/"* + uv run --no-project --python "$(cat .python-version)" python -m scripts.publish_release \ + --repository "$RELEASE_REPOSITORY" --commit "$RELEASE_COMMIT" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8ef74adc13..85ec74c7e0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,37 +21,6 @@ concurrency: cancel-in-progress: true jobs: - version-policy: - name: Version policy - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - ref: ${{ github.event.pull_request.head.sha }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - fetch-depth: 0 - - - name: Fetch target revision - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - BASE_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name }} - run: git fetch --no-tags "https://github.com/$BASE_REPOSITORY.git" "$BASE_SHA" - - - name: Set up CI environment - uses: ./.github/actions/ci-environment - with: - uv-version: ${{ env.CI_UV_VERSION }} - runner-label: ubuntu-latest - - - name: Check version policy - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: uv run --locked --group dev python scripts/check_version_policy.py --base "$BASE_SHA" --head "$HEAD_SHA" - no-suppressions-or-legacy-annotations: name: Ban suppressions and legacy annotations runs-on: ubuntu-latest @@ -63,7 +32,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.ref }} repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - fetch-depth: 1 + fetch-depth: 0 - name: "Fail on type ignores and legacy future annotations" run: | @@ -94,7 +63,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.ref }} repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - fetch-depth: 1 + fetch-depth: 0 - name: Set up CI environment uses: ./.github/actions/ci-environment @@ -126,7 +95,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.ref }} repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - fetch-depth: 1 + fetch-depth: 0 - name: Set up CI environment uses: ./.github/actions/ci-environment @@ -159,7 +128,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.ref }} repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - fetch-depth: 1 + fetch-depth: 0 - name: Set up CI environment uses: ./.github/actions/ci-environment diff --git a/.github/workflows/validate-bug-report-version.yml b/.github/workflows/validate-bug-report-version.yml index e1db541218..27d180d98d 100644 --- a/.github/workflows/validate-bug-report-version.yml +++ b/.github/workflows/validate-bug-report-version.yml @@ -29,8 +29,8 @@ jobs: issue_number, }); const fieldPattern = "^### FCC version\\r?\\n+\\s*([^\\r\\n]+)"; - const versionPattern = "(?:^|[^0-9A-Za-z_.])v?([0-9]+\\.[0-9]+\\.[0-9]+)(?![0-9A-Za-z_+-]|\\.[0-9A-Za-z_])"; - const projectVersionPattern = "^\\s*version\\s*=\\s*([\"'])([0-9]+\\.[0-9]+\\.[0-9]+)\\1\\s*(?:#.*)?$"; + const releaseVersionPattern = "^v((?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*))$"; + const versionPattern = "(?:^|[^0-9A-Za-z_.])v?([0-9]+\\.[0-9]+\\.[0-9]+(?:\\.dev[0-9]+(?:\\+g[0-9a-f]+(?:\\.d[0-9]{8})?)?|\\+d[0-9]{8})?)(?![0-9A-Za-z_+-]|\\.[0-9A-Za-z_])"; const match = (issue.body || "").match(new RegExp(fieldPattern, "m")); const fieldValue = match?.[1]?.trim() || ""; const versionMatches = [...fieldValue.matchAll(new RegExp(versionPattern, "g"))]; @@ -70,22 +70,6 @@ jobs: return false; }; - const projectVersionFromToml = (pyproject) => { - let inProjectSection = false; - for (const line of pyproject.split(/\r?\n/)) { - const stripped = line.trim(); - if (stripped === "[project]") { - inProjectSection = true; - continue; - } - if (inProjectSection && stripped.startsWith("[")) return undefined; - if (!inProjectSection) continue; - const projectVersion = line.match(new RegExp(projectVersionPattern)); - if (projectVersion !== null) return projectVersion[2]; - } - return undefined; - }; - if (!valid) { await deleteManagedComment(outdatedComment); if (!labels.has(labelName)) { @@ -98,7 +82,7 @@ jobs: ...repo, name: labelName, color: "d4c5f9", - description: "FCC version must contain number.number.number or be None", + description: "FCC version must contain one installed version or be None", }); } catch (createError) { if (createError.status !== 422) throw createError; @@ -114,7 +98,7 @@ jobs: await github.rest.issues.createComment({ ...repo, issue_number, - body: `${invalidMarker}\nPlease edit **FCC version** so it contains one \`number.number.number\` value, such as \`1.22.333\`, or is exactly \`None\`. Run \`fcc-server --version\` to find the installed version.`, + body: `${invalidMarker}\nPlease edit **FCC version** so it contains one version copied from \`fcc-server --version\`, including any development suffix, or is exactly \`None\`.`, }); } return; @@ -129,29 +113,23 @@ jobs: } await deleteManagedComment(invalidComment); - if (reportedVersion === undefined) { + // Checkout versions identify development source, not published releases. + if (reportedVersion === undefined || reportedVersion.includes(".dev") || reportedVersion.includes("+")) { await deleteManagedComment(outdatedComment); return; } - const defaultBranch = context.payload.repository.default_branch; - const { data: versionFile } = await github.rest.repos.getContent({ - ...repo, - path: "pyproject.toml", - ref: defaultBranch, - }); - if ( - Array.isArray(versionFile) - || versionFile.type !== "file" - || versionFile.encoding !== "base64" - || !versionFile.content - ) { - throw new Error(`Could not read pyproject.toml from ${defaultBranch}`); + let release; + try { + ({ data: release } = await github.rest.repos.getLatestRelease(repo)); + } catch (error) { + if (error.status !== 404) throw error; + await deleteManagedComment(outdatedComment); + return; } - const pyproject = Buffer.from(versionFile.content, "base64").toString("utf8"); - const latestVersion = projectVersionFromToml(pyproject); - if (latestVersion === undefined) { - throw new Error(`Could not read the project version from ${defaultBranch}`); + const latestVersion = release.tag_name.match(new RegExp(releaseVersionPattern))?.[1]; + if (!latestVersion || release.draft || release.prerelease) { + throw new Error("Latest release must have a stable vMAJOR.MINOR.PATCH tag"); } if (!isOlderVersion(reportedVersion, latestVersion)) { @@ -159,7 +137,7 @@ jobs: return; } - const body = `${outdatedMarker}\nThis report uses FCC \`${reportedVersion}\`, while the current version on \`${defaultBranch}\` is \`${latestVersion}\`. Please [update FCC](${context.payload.repository.html_url}#install), restart FCC, and check whether the issue still occurs. The update may already contain the fix.`; + const body = `${outdatedMarker}\nThis report uses FCC \`${reportedVersion}\`, while the latest published version is \`${latestVersion}\`. Please [update FCC](${context.payload.repository.html_url}#update), restart FCC, and check whether the issue still occurs. The update may already contain the fix.`; if (outdatedComment === undefined) { await github.rest.issues.createComment({ ...repo, diff --git a/.github/workflows/version-policy.yml b/.github/workflows/version-policy.yml new file mode 100644 index 0000000000..483fc2f591 --- /dev/null +++ b/.github/workflows/version-policy.yml @@ -0,0 +1,51 @@ +name: Release intent + +env: + CI_UV_VERSION: "0.12.13" + UV_MALWARE_CHECK: "1" + UV_PREVIEW_FEATURES: "malware-check" + +on: + pull_request: + types: [opened, synchronize, reopened, edited, ready_for_review] + +concurrency: + group: release-intent-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + version-policy: + name: Version policy + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + pull-requests: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + fetch-depth: 0 + + - name: Read current pull request + env: + GH_TOKEN: ${{ github.token }} + PR_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + BASE_REPOSITORY_URL: ${{ github.server_url }}/${{ github.repository }}.git + run: | + gh api "repos/$PR_REPOSITORY/pulls/$PR_NUMBER" > "$RUNNER_TEMP/release-pr.json" + base=$(jq -r .base.sha "$RUNNER_TEMP/release-pr.json") + git fetch --no-tags "$BASE_REPOSITORY_URL" "$base" + + - name: Set up CI environment + uses: ./.github/actions/ci-environment + with: + uv-version: ${{ env.CI_UV_VERSION }} + runner-label: ubuntu-latest + + - name: Check version policy + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: uv run --locked python scripts/check_version_policy.py --head "$HEAD_SHA" --pr-json "$RUNNER_TEMP/release-pr.json" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1037a71cf0..3a3c77416e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,6 +9,12 @@ Thanks for helping improve Free Claude Code. Keep changes focused, test the beha - For bugs, include every model mapping, the active model when the failure occurred, the complete error, and reproducible steps. - Add focused tests for behavior changes and relevant edge cases. +## Pull Request Titles + +Changes to `assets/`, `scripts/`, `src/`, `.python-version`, `pyproject.toml`, or `uv.lock` require a title starting with `patch: `, `minor: `, or `major: ` followed by a description. Use exactly one space after the colon. + +PRs without changes to those paths must not use a release prefix. Do not edit version numbers. Publishing calculates the version after merge, creates its Git tag, and publishes to PyPI and GitHub Releases. + ## Development Setup Install [uv](https://docs.astral.sh/uv/) and Python 3.14, then run directly from the checkout: @@ -54,7 +60,3 @@ GitHub CI runs Ruff in check-only mode and also bans `# type: ignore`, `# ty: ig - Keep shared Anthropic protocol behavior under `src/free_claude_code/core/anthropic/` rather than importing utilities from another provider. - Keep provider-specific configuration in the provider that owns it. - Remove dead compatibility code when completing migrations unless preserving a published interface is explicitly required. - -## Versioning - -Changes to runtime code, packaging, dependencies, or install/CI scripts require a semantic version bump in `pyproject.toml` and a matching `uv lock` update in the same commit. Documentation, tests, smoke coverage, and repository configuration do not require a version bump by themselves. diff --git a/pyproject.toml b/pyproject.toml index 4e62a6cdb7..ee74e01d6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling", "hatch-vcs>=0.5.0"] build-backend = "hatchling.build" [project] name = "free-claude-code" -version = "6.2.66" +dynamic = ["version"] description = "Local proxy connecting coding agents to OpenAI-compatible AI providers" readme = "README.md" license = "AGPL-3.0-only" @@ -63,6 +63,9 @@ voice_local = [ "librosa>=1.0.0", ] +[tool.hatch.version] +source = "vcs" + [tool.hatch.build.targets.wheel] packages = ["src/free_claude_code"] @@ -72,6 +75,7 @@ packages = ["src/free_claude_code"] [tool.uv] required-version = ">=0.12.13" +cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true } }] [tool.uv.sources] torch = { index = "pytorch-cu130" } diff --git a/scripts/check_version_policy.py b/scripts/check_version_policy.py index 6514ea8758..84de950c80 100644 --- a/scripts/check_version_policy.py +++ b/scripts/check_version_policy.py @@ -1,122 +1,125 @@ -"""Require one version increment exactly when a PR changes release inputs.""" +"""Validate release intent against changed files and calculate one version bump.""" import argparse +import json +import re import subprocess import tomllib - -import tomlkit -from packaging.version import Version +from pathlib import Path RELEASE_FILES = {".python-version", "pyproject.toml", "uv.lock"} RELEASE_DIRS = ("assets/", "scripts/", "src/") +VERSION = re.compile(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)") +PREFIX = re.compile(r"(patch|minor|major): \S[^\r\n]*") +RESERVED = re.compile(r"\s*(patch|minor|major)\s*[:\uff1a]", re.IGNORECASE) def git(*args: str) -> str: return subprocess.run( ["git", *args], check=True, capture_output=True, text=True, encoding="utf-8" - ).stdout.strip() + ).stdout.rstrip("\r\n") -def blob(revision: str, path: str) -> str | None: - result = subprocess.run( - ["git", "show", f"{revision}:{path}"], - check=False, - capture_output=True, - text=True, - encoding="utf-8", +def changed_paths(base: str, head: str) -> list[str]: + return list( + filter( + None, + git("diff", "--no-renames", "--name-only", "-z", base, head).split("\0"), + ) ) - return result.stdout if result.returncode == 0 else None -def root_package(document: dict) -> dict: - entries = [ - entry - for entry in document.get("package", []) - if entry.get("name") == "free-claude-code" - and entry.get("source") == {"editable": "."} +def release_paths(paths: list[str]) -> list[str]: + return [ + path for path in paths if path in RELEASE_FILES or path.startswith(RELEASE_DIRS) ] - if len(entries) != 1: - raise ValueError("uv.lock must contain exactly one editable FCC package") - return entries[0] - - -def version_at(revision: str) -> str: - project = tomllib.loads(blob(revision, "pyproject.toml") or "")["project"] - if project["name"] != "free-claude-code": - raise ValueError("Project name must be free-claude-code") - value = project["version"] - if not isinstance(value, str): - raise ValueError("Project version must be a string") - parsed = Version(value) - if value != f"{parsed.major}.{parsed.minor}.{parsed.micro}": - raise ValueError("Project version must use the format MAJOR.MINOR.PATCH") - lock = root_package(tomllib.loads(blob(revision, "uv.lock") or "")) - if lock["version"] != value: - raise ValueError("pyproject.toml and uv.lock versions must agree") - return value - - -def without_release_version(text: str | None, path: str) -> str | None: - if text is None: - return None - document = tomlkit.parse(text) - if path == "pyproject.toml": - document["project"]["version"] = "FCC_VERSION" - else: - for package in document["package"]: - if package["name"] == "free-claude-code" and package.get("source") == { - "editable": "." - }: - package["version"] = "FCC_VERSION" - return tomlkit.dumps(document) - - -def check(base: str, head: str) -> None: - base = git("rev-parse", "--verify", "--end-of-options", f"{base}^{{commit}}") - head = git("rev-parse", "--verify", "--end-of-options", f"{head}^{{commit}}") - ancestor = git("merge-base", base, head) - old, new = version_at(base), version_at(head) - changed = git("diff", "--no-renames", "--name-only", "-z", ancestor, head).split( - "\0" - ) - release_paths = [] - for path in changed: - if path not in RELEASE_FILES and not path.startswith(RELEASE_DIRS): - continue - if path in {"pyproject.toml", "uv.lock"} and without_release_version( - blob(ancestor, path), path - ) == without_release_version(blob(head, path), path): - continue - release_paths.append(path) - print(f"FCC version: {old} -> {new}") - print("Release changes: " + (", ".join(release_paths) or "none")) - previous = Version(old) - allowed = { - f"{previous.major}.{previous.minor}.{previous.micro + 1}", - f"{previous.major}.{previous.minor + 1}.0", - f"{previous.major + 1}.0.0", - } - if release_paths and new not in allowed: + + +def release_kind(title: str, paths: list[str]) -> str | None: + match = PREFIX.fullmatch(title) + released = release_paths(paths) + if not match and RESERVED.match(title): raise ValueError( - "Version must increase by exactly one patch, minor, or major increment " - "from the target branch, resetting lower components to zero. " - f"Allowed versions: {', '.join(sorted(allowed))}" + "Use exactly patch: , minor: , or major: followed by a description" ) - if not release_paths and new != old: - raise ValueError("Version must stay unchanged without release changes") + if released and not match: + raise ValueError("Release changes require a patch: , minor: , or major: title") + if not released and match: + raise ValueError("A non-release PR must not use a release prefix") + return match[1] if match else None + + +def next_version(previous: str, kind: str) -> str: + match = VERSION.fullmatch(previous) + if not match: + raise ValueError(f"Invalid release version: {previous}") + major, minor, patch = map(int, match.groups()) + if kind == "major": + return f"{major + 1}.0.0" + if kind == "minor": + return f"{major}.{minor + 1}.0" + if kind == "patch": + return f"{major}.{minor}.{patch + 1}" + raise ValueError(f"Invalid release kind: {kind}") + + +def check_packaging(head: str) -> None: + project = tomllib.loads(git("show", f"{head}:pyproject.toml"))["project"] + if ( + project["name"] != "free-claude-code" + or "version" in project + or "version" not in project.get("dynamic", []) + ): + raise ValueError("FCC must declare a dynamic version without project.version") + lock = tomllib.loads(git("show", f"{head}:uv.lock")) + roots = [ + p + for p in lock["package"] + if p["name"] == "free-claude-code" and p.get("source") == {"editable": "."} + ] + if len(roots) != 1 or "version" in roots[0]: + raise ValueError("uv.lock must have one editable FCC package without a version") + + +def check(base: str, head: str, title: str) -> None: + base = git("rev-parse", "--verify", "--end-of-options", f"{base}^{{commit}}") + head = git("rev-parse", "--verify", "--end-of-options", f"{head}^{{commit}}") + paths = changed_paths(git("merge-base", base, head), head) + print("Release changes: " + (", ".join(release_paths(paths)) or "none")) + kind = release_kind(title, paths) + check_packaging(head) + print(f"Version policy passed: {kind or 'no release'}") def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--base", required=True) + parser.add_argument("--base") parser.add_argument("--head", required=True) + parser.add_argument("--title") + parser.add_argument("--pr-json", type=Path) args = parser.parse_args() try: - check(args.base, args.head) - except (ValueError, KeyError, TypeError, subprocess.CalledProcessError) as error: + if args.pr_json: + pr = json.loads(args.pr_json.read_text(encoding="utf-8")) + if pr["head"]["sha"] != git( + "rev-parse", "--verify", "--end-of-options", f"{args.head}^{{commit}}" + ): + raise ValueError("PR head changed; use the current policy run") + base, title = pr["base"]["sha"], pr["title"] + else: + base, title = args.base, args.title + if not base or title is None: + raise ValueError("Provide --pr-json or both --base and --title") + check(base, args.head, title) + except ( + ValueError, + KeyError, + TypeError, + OSError, + subprocess.CalledProcessError, + ) as error: print(f"Version policy failed: {error}") return 1 - print("Version policy passed") return 0 diff --git a/scripts/publish_release.py b/scripts/publish_release.py new file mode 100644 index 0000000000..e8d56179c1 --- /dev/null +++ b/scripts/publish_release.py @@ -0,0 +1,207 @@ +"""Publish one main commit, resuming its tag and staged distribution files.""" + +import argparse +import hashlib +import json +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +from .check_version_policy import ( + VERSION, + changed_paths, + git, + next_version, + release_kind, + release_paths, +) + + +def command(args: list[str], *, cwd: Path | None = None) -> str: + return subprocess.run( + args, cwd=cwd, check=True, capture_output=True, text=True, encoding="utf-8" + ).stdout + + +def distribution_names(version: str) -> tuple[str, str]: + stem = f"free_claude_code-{version}" + return f"{stem}-py3-none-any.whl", f"{stem}.tar.gz" + + +class Publisher: + def __init__(self, repository: str) -> None: + if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository): + raise ValueError("Expected owner/repository") + self.repository = repository + self.validator = Path(__file__).with_name("validate_release.py").resolve() + + def gh(self, *args: str) -> str: + return command(["gh", "release", *args, "--repo", self.repository]) + + def releases(self) -> dict: + pages = json.loads( + command( + [ + "gh", + "api", + "--paginate", + "--slurp", + f"repos/{self.repository}/releases?per_page=100", + ] + ) + ) + return {release["tag_name"]: release for page in pages for release in page} + + def publish(self, target: str) -> None: + if not re.fullmatch(r"[0-9a-f]{40}", target): + raise ValueError("Release target must be a full commit SHA") + command(["git", "fetch", "--tags", "origin", "main"]) + history = git("rev-list", "--first-parent", "origin/main").splitlines() + if target not in history: + raise ValueError("Release target is not on main's first-parent history") + title = git("show", "-s", "--format=%s", target) + kind = release_kind(title, changed_paths(f"{target}^", target)) + if kind is None: + print(f"No release for {target}") + return + + tags = {} + # Peeled annotated-tag entries follow their unpeeled entries. + for line in git("show-ref", "--tags", "--dereference").splitlines(): + sha, ref = line.split() + name = ref.removeprefix("refs/tags/").removesuffix("^{}") + if name.startswith("v") and VERSION.fullmatch(name[1:]): + tags[name] = sha + previous = None + for sha in history[history.index(target) + 1 :]: + names = [name for name, commit in tags.items() if commit == sha] + if names: + if len(names) != 1: + raise ValueError(f"Ambiguous release tags at {sha}") + previous = names[0] + break + if release_paths(changed_paths(f"{sha}^", sha)): + raise ValueError( + f"Earlier release {sha} is unfinished; retry its post-merge run" + ) + if previous is None: + raise ValueError("No baseline release tag found") + releases = self.releases() + predecessor = releases.get(previous) + if not predecessor or predecessor["draft"] or predecessor["prerelease"]: + raise ValueError( + f"Earlier release {previous} is unfinished; retry its post-merge run" + ) + version = next_version(previous[1:], kind) + tag = f"v{version}" + if any(sha == target and name != tag for name, sha in tags.items()): + raise ValueError("Target already has another version") + if tag in tags and tags[tag] != target: + raise ValueError(f"Tag {tag} belongs to a different commit") + release = releases.get(tag) + if release and tag not in tags: + raise ValueError(f"Release {tag} has no matching remote tag") + if release and release["prerelease"]: + raise ValueError(f"Release {tag} unexpectedly is a prerelease") + if release and not release["draft"]: + print(f"{tag} already published") + return + if tag not in tags: + command(["git", "tag", tag, target]) + command(["git", "push", "origin", f"refs/tags/{tag}"]) + if release is None: + self.gh( + "create", + tag, + "--draft", + "--verify-tag", + "--title", + tag, + "--generate-notes", + "--notes-start-tag", + previous, + ) + print(f"Publishing {tag} from {target}") + with tempfile.TemporaryDirectory(prefix="fcc-release-") as temporary: + root = Path(temporary).resolve() + source, dist = root / "source", root / "dist" + dist.mkdir() + command(["git", "worktree", "add", "--detach", str(source), target]) + try: + if command(["git", "status", "--porcelain"], cwd=source).strip(): + raise ValueError("Release source must be clean") + self.stage(tag, version, source, dist) + command( + [ + "uv", + "publish", + "--trusted-publishing", + "always", + *(str(dist / name) for name in distribution_names(version)), + ] + ) + self.gh("edit", tag, "--draft=false", "--latest", "--verify-tag") + finally: + if not source.is_relative_to(root): + raise ValueError("Release worktree escaped its temporary directory") + command(["git", "worktree", "remove", "--force", str(source)]) + + def validate(self, source: Path, dist: Path, version: str) -> None: + command( + [sys.executable, str(self.validator), str(dist), "--version", version], + cwd=source, + ) + + def stage(self, tag: str, version: str, source: Path, dist: Path) -> None: + names = set(distribution_names(version)) + assets = self.releases()[tag]["assets"] + if any(asset["name"] not in names for asset in assets): + raise ValueError(f"Unexpected assets on {tag}") + complete = ( + {asset["name"] for asset in assets} == names + and len(assets) == 2 + and all(asset["state"] == "uploaded" for asset in assets) + ) + if complete: + self.gh("download", tag, "--dir", str(dist)) + else: + # Publication never begins until both uploaded assets are verified. + for asset in assets: + self.gh("delete-asset", tag, asset["name"], "--yes") + command(["uv", "build", "--no-sources", "--out-dir", str(dist)], cwd=source) + self.validate(source, dist, version) + self.gh("upload", tag, *(str(dist / name) for name in sorted(names))) + assets = self.releases()[tag]["assets"] + if {asset["name"] for asset in assets} != names or len(assets) != 2: + raise ValueError(f"Incomplete staged distributions for {tag}") + for asset in assets: + data = (dist / asset["name"]).read_bytes() + digest = "sha256:" + hashlib.sha256(data).hexdigest() + if ( + asset["state"] != "uploaded" + or asset["size"] != len(data) + or asset.get("digest") != digest + ): + raise ValueError(f"Staged asset failed verification: {asset['name']}") + self.validate(source, dist, version) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repository", required=True) + parser.add_argument("--commit", required=True) + args = parser.parse_args() + try: + Publisher(args.repository).publish(args.commit) + except (ValueError, KeyError, OSError, subprocess.CalledProcessError) as error: + print(f"Release failed: {error}", file=sys.stderr) + if isinstance(error, subprocess.CalledProcessError): + print(error.stderr, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate_release.py b/scripts/validate_release.py index 02f80297c8..69274dce69 100644 --- a/scripts/validate_release.py +++ b/scripts/validate_release.py @@ -2,6 +2,7 @@ import argparse import configparser +import re import subprocess import tarfile import tomllib @@ -16,11 +17,17 @@ def require_files(names: set[str], expected: set[str]) -> None: raise ValueError("Missing release files: " + ", ".join(sorted(missing))) -def validate(directory: Path) -> None: +def validate(directory: Path, version: str) -> None: project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))[ "project" ] - name, version = project["name"], project["version"] + name = project["name"] + if not re.fullmatch( + r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", version + ): + raise ValueError("Expected a stable MAJOR.MINOR.PATCH release version") + if "version" in project or "version" not in project.get("dynamic", []): + raise ValueError("Release source must use dynamic version metadata") stem = f"{name.replace('-', '_')}-{version}" wheel_path = directory / f"{stem}-py3-none-any.whl" sdist_path = directory / f"{stem}.tar.gz" @@ -90,9 +97,10 @@ def validate(directory: Path) -> None: def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("directory", type=Path) + parser.add_argument("--version", required=True) args = parser.parse_args() try: - validate(args.directory) + validate(args.directory, args.version) except ( ValueError, KeyError, diff --git a/tests/contracts/test_issue_form_version_validation.py b/tests/contracts/test_issue_form_version_validation.py index 4ac37c8cf0..2e797653cf 100644 --- a/tests/contracts/test_issue_form_version_validation.py +++ b/tests/contracts/test_issue_form_version_validation.py @@ -3,7 +3,6 @@ import shutil import subprocess import textwrap -import tomllib from pathlib import Path from typing import Any @@ -62,7 +61,6 @@ def test_bug_form_requests_a_contained_version_or_none() -> None: assert "Run `fcc-server --version`" in form assert "include one version" in form - assert "`number.number.number` format" in form assert "enter `None`" in form assert 'placeholder: "The version is 1.22.333, or None"' in form assert "not installed" not in form @@ -165,39 +163,21 @@ def test_workflow_owns_one_idempotent_triage_state() -> None: assert "comments.find" in workflow -def test_workflow_reads_and_compares_the_default_branch_version() -> None: - workflow = WORKFLOW.read_text(encoding="utf-8") - pyproject = Path("pyproject.toml").read_text(encoding="utf-8") - expected = tomllib.loads(pyproject)["project"]["version"] - project_pattern = _workflow_pattern("projectVersionPattern") - function = _javascript_function("projectVersionFromToml") - scoped_project = ( - "[tool.before]\nversion = \"99.0.0\"\n\n[project]\nversion = '1.2.3'\n" - ) - commented_project = ( - '[project]\nname = "demo"\nversion = "2.3.4" # current release\n' - ) - missing_project_version = ( - '[project]\nname = "demo"\n\n[[tool.items]]\nversion = "99.0.0"\n' - ) - script = ( - f"const projectVersionPattern = {json.dumps(project_pattern)};\n" - f"{function}\n" - "process.stdout.write(JSON.stringify([" - f"projectVersionFromToml({json.dumps(pyproject)})," - f"projectVersionFromToml({json.dumps(scoped_project)})," - f"projectVersionFromToml({json.dumps(commented_project)})," - f"projectVersionFromToml({json.dumps(missing_project_version)})" - "]));" - ) - - assert _run_javascript(script) == [expected, "1.2.3", "2.3.4", None] - assert "contents: read" in workflow - assert "github.rest.repos.getContent" in workflow - assert 'path: "pyproject.toml"' in workflow - assert "context.payload.repository.default_branch" in workflow - assert 'split(".").map((part) => BigInt(part))' in workflow - assert "isOlderVersion(reportedVersion, latestVersion)" in workflow +@pytest.mark.parametrize( + "tag,expected", + [ + ("v1.2.3", "1.2.3"), + ("v10.0.0", "10.0.0"), + ("1.2.3", None), + ("v1.2.3rc1", None), + ("v01.2.3", None), + ("v1.2.3.4", None), + ], +) +def test_release_tags_are_exact_stable_versions(tag, expected): + pattern = _workflow_pattern("releaseVersionPattern") + match = re.fullmatch(pattern, tag) + assert (match[1] if match else None) == expected def test_outdated_version_comment_is_reconciled_across_edits() -> None: @@ -248,16 +228,9 @@ def test_outdated_version_comment_is_reconciled_across_edits() -> None: }, }, repos: { - getContent: async (args) => { - record("getContent", args); - const content = `[tool.before]\nversion = "99.0.0"\n\n[project]\nversion = '${latestVersion}' # current release\n`; - return { - data: { - type: "file", - encoding: "base64", - content: Buffer.from(content).toString("base64"), - }, - }; + getLatestRelease: async (args) => { + record("getLatestRelease", args); + return { data: { tag_name: "v" + latestVersion, draft: false, prerelease: false } }; }, }, }, @@ -301,19 +274,16 @@ def test_outdated_version_comment_is_reconciled_across_edits() -> None: ) calls = result["calls"] names = [call["name"] for call in calls] - content_reads = [call for call in calls if call["name"] == "getContent"] assert names.count("createComment") == 2 assert names.count("updateComment") == 1 assert names.count("deleteComment") == 3 - assert names.count("getContent") == 4 + assert names.count("getLatestRelease") == 4 assert names.count("getIssue") == 6 assert names.count("getLabel") == 1 assert names.count("addLabels") == 1 assert names.count("removeLabel") == 2 assert "createLabel" not in names - assert all(call["args"]["path"] == "pyproject.toml" for call in content_reads) - assert all(call["args"]["ref"] == "main" for call in content_reads) assert "`17.23.454`" in next( call["args"]["body"] for call in calls @@ -330,3 +300,87 @@ def test_outdated_version_comment_is_reconciled_across_edits() -> None: assert "cancel-in-progress: false" in workflow assert "github.rest.issues.update({" not in workflow assert 'state: "closed"' not in workflow + + +@pytest.mark.parametrize("status,should_fail", [(404, False), (500, True)]) +def test_release_lookup_distinguishes_no_release_from_failure(status, should_fail): + source = f"return (async () => {{\n{_workflow_script()}\n}})();" + harness = """ +const run = new Function("github", "context", __SOURCE__); +const deleted = []; +const github = { + paginate: async () => [{id: 42, user: {login: "github-actions[bot]"}, body: ""}], + rest: { + issues: { + get: async () => ({data: {labels: [], body: "### FCC version\\n\\n1.2.3"}}), + deleteComment: async ({comment_id}) => deleted.push(comment_id), + }, + repos: {getLatestRelease: async () => {throw {status: __STATUS__};}}, + }, +}; +let failed = false; +try {await run(github, {repo: {owner: "o", repo: "r"}, payload: {issue: {number: 1}}});} +catch {failed = true;} +process.stdout.write(JSON.stringify({failed, deleted})); +""" + result = _run_javascript( + harness.replace("__SOURCE__", json.dumps(source)).replace( + "__STATUS__", str(status) + ) + ) + assert result["failed"] is should_fail + assert result["deleted"] == ([] if should_fail else [42]) + + +@pytest.mark.parametrize( + "value", + [ + "6.2.67.dev1+g2f30121c0", + "6.2.67.dev0+g2f30121c0.d20260925", + "6.2.67.dev1", + "6.2.66+d20260925", + ], +) +def test_development_versions_are_accepted_without_stable_release_comparison(value): + assert _reported_version(f"free-claude-code {value}") == value + source = f"return (async () => {{\n{_workflow_script()}\n}})();" + harness = """ +const run = new Function("github", "context", __SOURCE__); +const calls = []; +const github = { + paginate: async () => [ + {id: 1, user: {login: "github-actions[bot]"}, body: ""}, + {id: 2, user: {login: "github-actions[bot]"}, body: ""}, + ], + rest: { + issues: { + get: async () => ({data: {labels: [{name: "needs-fcc-version"}], body: "### FCC version\\n\\nfree-claude-code " + __VERSION__}}), + removeLabel: async () => calls.push("removeLabel"), + deleteComment: async ({comment_id}) => calls.push(comment_id), + }, + repos: {getLatestRelease: async () => {throw new Error("Development builds must not be compared as stable releases");}}, + }, +}; +await run(github, {repo: {owner: "o", repo: "r"}, payload: {issue: {number: 1}}}); +process.stdout.write(JSON.stringify(calls)); +""" + result = _run_javascript( + harness.replace("__SOURCE__", json.dumps(source)).replace( + "__VERSION__", json.dumps(value) + ) + ) + assert result == ["removeLabel", 1, 2] + + +@pytest.mark.parametrize( + "value", + [ + "6.2.67.dev", + "6.2.67.dev1+gwrong", + "6.2.67.dev1+g123.extra", + "6.2.67.dev1+g123.d2026", + "6.2.67.dev1+g123 and 6.2.66", + ], +) +def test_invalid_or_ambiguous_development_versions_are_rejected(value): + assert _reported_version(value) is None diff --git a/tests/contracts/test_uv_policy.py b/tests/contracts/test_uv_policy.py index ae66e85d68..086dc888df 100644 --- a/tests/contracts/test_uv_policy.py +++ b/tests/contracts/test_uv_policy.py @@ -9,6 +9,7 @@ UV_WORKFLOWS = ( Path(".github/workflows/tests.yml"), Path(".github/workflows/post-merge.yml"), + Path(".github/workflows/version-policy.yml"), ) REQUIRED_TEST_RUNNERS = { "Linux": "ubuntu-latest", diff --git a/tests/core/test_version.py b/tests/core/test_version.py index 084409bb82..dcd94adbde 100644 --- a/tests/core/test_version.py +++ b/tests/core/test_version.py @@ -1,7 +1,5 @@ -import tomllib from importlib.metadata import PackageNotFoundError from importlib.metadata import version as distribution_version -from pathlib import Path import pytest @@ -33,10 +31,3 @@ def invalid(_distribution_name: str) -> str: with pytest.raises(ValueError, match="invalid metadata"): version_module.package_version() - - -def test_project_release_version_matches_installed_metadata() -> None: - repo_root = Path(__file__).resolve().parents[2] - pyproject = tomllib.loads((repo_root / "pyproject.toml").read_text("utf-8")) - - assert pyproject["project"]["version"] == distribution_version("free-claude-code") diff --git a/tests/scripts/test_publish_release.py b/tests/scripts/test_publish_release.py new file mode 100644 index 0000000000..05f0abd746 --- /dev/null +++ b/tests/scripts/test_publish_release.py @@ -0,0 +1,264 @@ +"""Release ordering and interrupted publication use real Git history.""" + +import hashlib +import json +from pathlib import Path +from typing import TypedDict + +import pytest + +from scripts import publish_release as publishing +from scripts.check_version_policy import next_version +from tests.scripts.test_version_policy import commit, git, write + + +@pytest.mark.parametrize( + "kind,expected", [("patch", "6.2.67"), ("minor", "6.3.0"), ("major", "7.0.0")] +) +def test_one_increment(kind, expected): + assert next_version("6.2.66", kind) == expected + + +@pytest.fixture +def releases(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + git(tmp_path, "init", "-b", "main") + write(tmp_path, "src/file.py", "base") + base = commit(tmp_path) + git(tmp_path, "tag", "v1.2.3") + git(tmp_path, "update-ref", "refs/remotes/origin/main", base) + state = FakeRemote(tmp_path) + monkeypatch.setattr(publishing, "command", state.command) + return tmp_path, state + + +class Asset(TypedDict): + name: str + state: str + size: int + digest: str + + +class Release(TypedDict): + tag_name: str + draft: bool + prerelease: bool + assets: list[Asset] + + +class FakeRemote: + def __init__(self, repo): + self.repo = repo + self.releases: dict[str, Release] = { + "v1.2.3": { + "tag_name": "v1.2.3", + "draft": False, + "prerelease": False, + "assets": [], + } + } + self.files = {} + self.calls = [] + self.fail = None + self.builds = 0 + self.uploaded = {} + self.real_command = publishing.command + + def command(self, args, *, cwd=None): + if "--repo" in args: + args = args[: args.index("--repo")] + self.calls.append(args) + if args[0] == "git": + if args[1] in {"fetch", "push"}: + return "" + return git(Path(cwd or self.repo), *args[1:]) + if args[:2] == ["gh", "api"]: + return json.dumps([list(self.releases.values())]) + if args[:3] == ["gh", "release", "create"]: + tag = args[3] + self.releases[tag] = { + "tag_name": tag, + "draft": True, + "prerelease": False, + "assets": [], + } + elif args[:3] == ["gh", "release", "upload"]: + tag = args[3] + for filename in args[4:]: + file = Path(filename) + data = file.read_bytes() + self.files[tag, file.name] = data + self.releases[tag]["assets"].append( + { + "name": file.name, + "state": "uploaded", + "size": len(data), + "digest": "sha256:" + hashlib.sha256(data).hexdigest(), + } + ) + if self.fail == "stage": + self.fail = None + raise RuntimeError("interrupted staging") + elif args[:3] == ["gh", "release", "delete-asset"]: + tag, name = args[3:5] + self.releases[tag]["assets"] = [ + a for a in self.releases[tag]["assets"] if a["name"] != name + ] + self.files.pop((tag, name), None) + elif args[:3] == ["gh", "release", "download"]: + tag = args[3] + dest = Path(args[args.index("--dir") + 1]) + for asset in self.releases[tag]["assets"]: + (dest / asset["name"]).write_bytes(self.files[tag, asset["name"]]) + elif args[:3] == ["gh", "release", "edit"]: + if self.fail == "finalize": + self.fail = None + raise RuntimeError("interrupted finalization") + self.releases[args[3]]["draft"] = False + elif args[:2] == ["uv", "build"]: + self.builds += 1 + assert cwd is not None + version = git(Path(cwd), "describe", "--tags", "--exact-match")[1:] + out = Path(args[args.index("--out-dir") + 1]) + for name in publishing.distribution_names(version): + (out / name).write_bytes(f"build {self.builds}: {name}".encode()) + elif args[:2] == ["uv", "publish"]: + for file in map(Path, args[4:]): + data = file.read_bytes() + if file.name in self.uploaded: + assert self.uploaded[file.name] == data + self.uploaded[file.name] = data + if self.fail == "publish": + self.fail = None + raise RuntimeError("interrupted publication") + elif args[0] == publishing.sys.executable: + pass # Archive validation is exercised separately using real archives. + else: + raise AssertionError(args) + return "" + + def advance(self, title="patch: Change", path="src/file.py"): + write(self.repo, path, title) + head = commit(self.repo, title) + git(self.repo, "update-ref", "refs/remotes/origin/main", head) + return head + + def publisher(self): + return publishing.Publisher("owner/repo") + + +def test_nonrelease_never_allocates_or_builds(releases): + _, remote = releases + head = remote.advance("Explain usage", "README.md") + remote.publisher().publish(head) + assert remote.builds == 0 + assert not any(c[:2] == ["git", "push"] for c in remote.calls) + + +@pytest.mark.parametrize("failure", ["stage", "publish", "finalize"]) +def test_retries_keep_version_and_published_bytes(releases, failure): + repo, remote = releases + head = remote.advance() + remote.fail = failure + with pytest.raises(RuntimeError): + remote.publisher().publish(head) + first_uploaded = dict(remote.uploaded) + assert remote.releases["v1.2.4"]["draft"] + remote.publisher().publish(head) + assert not remote.releases["v1.2.4"]["draft"] + assert git(repo, "rev-parse", "v1.2.4") == head + assert remote.builds == (2 if failure == "stage" else 1) + assert all(remote.uploaded[name] == value for name, value in first_uploaded.items()) + builds = remote.builds + remote.publisher().publish(head) + assert remote.builds == builds + + +def test_later_commit_waits_even_if_previous_failed_before_tag(releases): + _, remote = releases + first = remote.advance() + second = remote.advance("minor: Second") + with pytest.raises(ValueError, match=first): + remote.publisher().publish(second) + remote.publisher().publish(first) + remote.publisher().publish(second) + assert not remote.releases["v1.3.0"]["draft"] + + +def test_later_commit_waits_for_draft_and_old_retry_is_noop(releases): + _, remote = releases + first = remote.advance() + remote.fail = "publish" + with pytest.raises(RuntimeError): + remote.publisher().publish(first) + second = remote.advance("major: Second") + with pytest.raises(ValueError, match=r"v1\.2\.4"): + remote.publisher().publish(second) + remote.publisher().publish(first) + remote.publisher().publish(second) + calls = len(remote.calls) + remote.publisher().publish(first) + assert not any(c[:3] == ["gh", "release", "edit"] for c in remote.calls[calls:]) + + +def test_tag_collision_and_extra_target_tag_are_rejected(releases): + repo, remote = releases + head = remote.advance() + git(repo, "tag", "v1.2.4", "v1.2.3") + with pytest.raises(ValueError): + remote.publisher().publish(head) + git(repo, "tag", "-d", "v1.2.4") + git(repo, "tag", "v9.0.0", head) + with pytest.raises(ValueError, match="another version"): + remote.publisher().publish(head) + + +def test_target_must_be_main_ancestor(releases): + repo, remote = releases + git(repo, "checkout", "-b", "other") + write(repo, "src/other.py", "other") + head = commit(repo, "patch: Other") + with pytest.raises(ValueError, match="main"): + remote.publisher().publish(head) + + +def test_malformed_or_bypassed_title_cannot_publish(releases): + _, remote = releases + head = remote.advance("Oops") + with pytest.raises(ValueError, match="require"): + remote.publisher().publish(head) + + +def test_reserved_tag_and_missing_draft_can_resume(releases): + repo, remote = releases + head = remote.advance() + git(repo, "tag", "v1.2.4", head) + remote.publisher().publish(head) + assert remote.builds == 1 + assert not remote.releases["v1.2.4"]["draft"] + + +def test_staged_bytes_are_verified_before_any_retry_upload(releases): + _, remote = releases + head = remote.advance() + remote.fail = "publish" + with pytest.raises(RuntimeError): + remote.publisher().publish(head) + name = publishing.distribution_names("1.2.4")[0] + remote.files["v1.2.4", name] = b"corrupted" + before = len(remote.calls) + with pytest.raises(ValueError, match="verification"): + remote.publisher().publish(head) + assert not any(c[:2] == ["uv", "publish"] for c in remote.calls[before:]) + + +def test_no_release_does_not_wait_for_an_unfinished_release(releases): + _, remote = releases + first = remote.advance() + remote.fail = "publish" + with pytest.raises(RuntimeError): + remote.publisher().publish(first) + docs = remote.advance("Explain the release", "README.md") + before = remote.builds + remote.publisher().publish(docs) + assert remote.builds == before diff --git a/tests/scripts/test_release_package.py b/tests/scripts/test_release_package.py index 38293376ef..ab0bcde3fe 100644 --- a/tests/scripts/test_release_package.py +++ b/tests/scripts/test_release_package.py @@ -20,7 +20,7 @@ def release(tmp_path): repo.mkdir() git(repo, "init") files = { - "pyproject.toml": '[project]\nname="free-claude-code"\nversion="1.2.3"\n[project.scripts]\nfcc-server="free_claude_code.cli:serve"\n[project.gui-scripts]\nfcc-desktop="free_claude_code.desktop:launch"\n', + "pyproject.toml": '[project]\nname="free-claude-code"\ndynamic=["version"]\n[project.scripts]\nfcc-server="free_claude_code.cli:serve"\n[project.gui-scripts]\nfcc-desktop="free_claude_code.desktop:launch"\n', "README.md": "readme", "LICENSE": "license", "scripts/update/fcc-update": "#!/bin/sh\n", @@ -71,7 +71,7 @@ def validate(release, tmp_path): info.size = len(data) archive.addfile(info, io.BytesIO(data)) return subprocess.run( - [sys.executable, str(VALIDATOR), str(dist)], + [sys.executable, str(VALIDATOR), str(dist), "--version", "1.2.3"], cwd=repo, capture_output=True, text=True, diff --git a/tests/scripts/test_release_workflow.py b/tests/scripts/test_release_workflow.py index d0facb74c2..b75568139d 100644 --- a/tests/scripts/test_release_workflow.py +++ b/tests/scripts/test_release_workflow.py @@ -1,25 +1,24 @@ -"""Release decisions and workflow permissions are explicit contracts.""" +"""Workflow events, checkout identity and publishing privileges are contracts.""" -import os -import subprocess -import sys +import shlex from pathlib import Path import pytest import yaml -from tests.scripts.test_version_policy import commit, git, version, write +from tests.scripts.test_version_policy import commit, git, write ROOT = Path(__file__).resolve().parents[2] -WORKFLOW = ROOT / ".github/workflows/post-merge.yml" -def workflow(): - return yaml.safe_load(WORKFLOW.read_text(encoding="utf-8")) +def load(name): + return yaml.safe_load( + (ROOT / ".github/workflows" / name).read_text(encoding="utf-8") + ) -def test_post_merge_serializes_runs_and_separates_publish_permissions(): - config = workflow() +def test_post_merge_serializes_and_limits_publish_permissions(): + config = load("post-merge.yml") assert config["concurrency"] == { "group": "post-merge-main", "cancel-in-progress": False, @@ -27,7 +26,7 @@ def test_post_merge_serializes_runs_and_separates_publish_permissions(): } assert len(config["jobs"]) == 2 cache = config["jobs"]["warm-dependency-cache"] - assert {entry["os"] for entry in cache["strategy"]["matrix"]["include"]} == { + assert {e["os"] for e in cache["strategy"]["matrix"]["include"]} == { "Linux", "Windows", "macOS", @@ -35,73 +34,103 @@ def test_post_merge_serializes_runs_and_separates_publish_permissions(): publish = config["jobs"]["publish"] assert "needs" not in publish assert publish["environment"] == "pypi" - assert publish["permissions"] == {"contents": "read", "id-token": "write"} + assert publish["permissions"] == {"contents": "write", "id-token": "write"} assert cache["permissions"] == {"contents": "read"} for job in (cache, publish): assert job["if"] == "github.ref == 'refs/heads/main'" checkout = next( - step - for step in job["steps"] - if step.get("uses", "").startswith("actions/checkout@") + s for s in job["steps"] if s.get("uses", "").startswith("actions/checkout@") ) assert checkout["with"]["ref"] == "${{ github.sha }}" - for name in ("Build and validate release", "Publish release"): - step = next(step for step in publish["steps"] if step.get("name") == name) - assert step["if"] == "steps.release.outputs.changed == 'true'" + assert checkout["with"]["fetch-depth"] == 0 + step = next(s for s in publish["steps"] if s.get("name") == "Publish release") + assert step["env"]["RELEASE_COMMIT"] == "${{ inputs.release_commit || github.sha }}" + assert "-m scripts.publish_release" in step["run"] -@pytest.mark.parametrize("bump", [False, True]) -@pytest.mark.parametrize("manual", [False, True]) -def test_release_detection_uses_the_triggering_revision(tmp_path, bump, manual): - git(tmp_path, "init", "-b", "main") - version(tmp_path, "1.2.3") - before = commit(tmp_path) - version(tmp_path, "1.2.4" if bump else "1.2.3") - write(tmp_path, "README.md", "changed\n") - head = commit(tmp_path) - # A later checkout/commit must not change the queued run's release decision. - version(tmp_path, "9.0.0") - commit(tmp_path) - steps = workflow()["jobs"]["publish"]["steps"] - run = next(step["run"] for step in steps if step.get("id") == "release") - source = run.split("<<'PY'\n", 1)[1].rsplit("\nPY", 1)[0] - output = tmp_path / "outputs" - result = subprocess.run( - [sys.executable, "-c", source], - cwd=tmp_path, - env=os.environ - | { - "BEFORE_SHA": "" if manual else before, - "HEAD_SHA": head, - "GITHUB_OUTPUT": str(output), - }, - capture_output=True, - text=True, - check=False, +def test_title_edits_only_trigger_the_separate_required_check(): + policy = load("version-policy.yml") + tests = load("tests.yml") + assert set(policy[True]) == {"pull_request"} + assert set(policy[True]["pull_request"]["types"]) == { + "opened", + "synchronize", + "reopened", + "edited", + "ready_for_review", + } + assert tests[True]["pull_request"] is None + assert policy["concurrency"]["group"] != tests["concurrency"]["group"] + assert "version-policy" not in tests["jobs"] + job = policy["jobs"]["version-policy"] + assert job["name"] == "Version policy" + assert job["permissions"] == {"contents": "read", "pull-requests": "read"} + checkout = next( + s for s in job["steps"] if s.get("uses", "").startswith("actions/checkout@") ) - assert result.returncode == 0, result.stdout + result.stderr - assert output.read_text().strip() == f"changed={str(bump).lower()}" + assert checkout["with"]["ref"] == "${{ github.event.pull_request.head.sha }}" + assert checkout["with"]["fetch-depth"] == 0 + current = next( + s for s in job["steps"] if s.get("name") == "Read current pull request" + ) + assert 'gh api "repos/$PR_REPOSITORY/pulls/$PR_NUMBER"' in current["run"] + check = next(s for s in job["steps"] if s.get("name") == "Check version policy") + assert "--pr-json" in check["run"] + assert "title" not in check["env"] -def test_version_check_receives_pr_base_and_head_without_publish_permission(): - config = yaml.safe_load( - (ROOT / ".github/workflows/tests.yml").read_text(encoding="utf-8") - ) - assert set(config[True]) == {"pull_request"} - job = config["jobs"]["version-policy"] - checkout = next( - step - for step in job["steps"] - if step.get("uses", "").startswith("actions/checkout@") +def test_all_project_setup_jobs_have_history_and_fetch_canonical_tags(): + for filename in ("tests.yml", "version-policy.yml", "post-merge.yml"): + for job in load(filename)["jobs"].values(): + if any( + s.get("uses") == "./.github/actions/ci-environment" + for s in job["steps"] + ): + checkout = next( + s + for s in job["steps"] + if s.get("uses", "").startswith("actions/checkout@") + ) + assert checkout["with"]["fetch-depth"] == 0 + setup = yaml.safe_load( + (ROOT / ".github/actions/ci-environment/action.yml").read_text() ) - assert checkout["with"]["ref"] == "${{ github.event.pull_request.head.sha }}" - assert job["name"] == "Version policy" - assert job["permissions"] == {"contents": "read"} - step = next( - step for step in job["steps"] if step.get("name") == "Check version policy" + fetch = setup["runs"]["steps"][0] + assert fetch["env"]["CI_REPOSITORY"] == "${{ github.repository }}" + + +@pytest.mark.parametrize("conflicting_tag", [False, True]) +def test_ci_tag_fetch_uses_only_canonical_tags(tmp_path, conflicting_tag): + upstream = tmp_path / "upstream.git" + upstream.mkdir() + git(upstream, "init", "-b", "main") + write(upstream, "file", "release") + released = commit(upstream) + git(upstream, "tag", "-a", "v1.2.3", "-m", "release") + checkout = tmp_path / "fork" + git(tmp_path, "clone", str(upstream), str(checkout)) + write(checkout, "file", "pull request") + head = commit(checkout) + git(checkout, "tag", "v99.0.0") + if conflicting_tag: + git(checkout, "tag", "-f", "v1.2.3") + write(upstream, "file", "new release") + latest = commit(upstream) + git(upstream, "tag", "v1.2.4") + + setup = yaml.safe_load( + (ROOT / ".github/actions/ci-environment/action.yml").read_text() ) - assert step["env"] == { - "BASE_SHA": "${{ github.event.pull_request.base.sha }}", - "HEAD_SHA": "${{ github.event.pull_request.head.sha }}", - } - assert '--base "$BASE_SHA" --head "$HEAD_SHA"' in step["run"] + command = shlex.split(setup["runs"]["steps"][0]["run"]) + command = [ + arg.replace("$CI_SERVER/$CI_REPOSITORY.git", upstream.as_posix()) + for arg in command + ] + assert command[0] == "git" + for _ in range(2): + git(checkout, *command[1:]) + assert git(checkout, "tag", "--list").splitlines() == ["v1.2.3", "v1.2.4"] + assert git(checkout, "rev-parse", "v1.2.3^{}") == released + assert git(checkout, "rev-parse", "v1.2.4") == latest + assert git(checkout, "rev-parse", "HEAD") == head + assert git(checkout, "status", "--porcelain") == "" diff --git a/tests/scripts/test_version_policy.py b/tests/scripts/test_version_policy.py index bcbbb5b32f..425460f063 100644 --- a/tests/scripts/test_version_policy.py +++ b/tests/scripts/test_version_policy.py @@ -1,4 +1,4 @@ -"""Exercise version policy against real, isolated Git history.""" +"""Release intent is validated against committed paths, not edited numbers.""" import os import subprocess @@ -11,7 +11,7 @@ def git(repo: Path, *args: str) -> str: - result = subprocess.run( + return subprocess.run( ["git", "-C", str(repo), *args], check=True, capture_output=True, @@ -25,8 +25,7 @@ def git(repo: Path, *args: str) -> str: "GIT_COMMITTER_NAME": "Test", "GIT_COMMITTER_EMAIL": "test@example.invalid", }, - ) - return result.stdout.strip() + ).stdout.strip() def write(repo: Path, path: str, content: str) -> None: @@ -35,22 +34,9 @@ def write(repo: Path, path: str, content: str) -> None: target.write_text(content, encoding="utf-8") -def version(repo: Path, value: str, lock: str | None = None) -> None: - write( - repo, - "pyproject.toml", - f'[project]\nname = "free-claude-code"\nversion = "{value}"\n', - ) - write( - repo, - "uv.lock", - f'[[package]]\nname = "free-claude-code"\nversion = "{lock or value}"\nsource = {{ editable = "." }}\n', - ) - - -def commit(repo: Path) -> str: +def commit(repo: Path, title: str = "test change") -> str: git(repo, "add", ".") - git(repo, "commit", "--no-gpg-sign", "-m", "test change") + git(repo, "commit", "--no-gpg-sign", "-m", title) return git(repo, "rev-parse", "HEAD") @@ -58,14 +44,32 @@ def commit(repo: Path) -> str: def history(tmp_path): git(tmp_path, "init", "-b", "main") git(tmp_path, "config", "core.autocrlf", "false") - version(tmp_path, "1.2.3") + write( + tmp_path, + "pyproject.toml", + '[project]\nname="free-claude-code"\ndynamic=["version"]\n', + ) + write( + tmp_path, + "uv.lock", + '[[package]]\nname="free-claude-code"\nsource={editable="."}\n', + ) write(tmp_path, "src/file.py", "original\n") return tmp_path, commit(tmp_path) -def check(repo: Path, base: str) -> subprocess.CompletedProcess[str]: +def check(repo: Path, base: str, title: str) -> subprocess.CompletedProcess[str]: return subprocess.run( - [sys.executable, str(CHECKER), "--base", base, "--head", "HEAD"], + [ + sys.executable, + str(CHECKER), + "--base", + base, + "--head", + "HEAD", + "--title", + title, + ], cwd=repo, capture_output=True, text=True, @@ -84,162 +88,125 @@ def check(repo: Path, base: str) -> subprocess.CompletedProcess[str]: "uv.lock", ], ) -@pytest.mark.parametrize("bump", [False, True]) -def test_release_changes_require_increase(history, path, bump): - repo, base = history - if bump: - version(repo, "1.2.4") - if path in {"pyproject.toml", "uv.lock"}: - with (repo / path).open("a", encoding="utf-8") as stream: - stream.write("# non-version change\n") - else: - write(repo, path, "changed\n") - commit(repo) - result = check(repo, base) - assert (result.returncode == 0) is bump, result.stdout + result.stderr - assert path in result.stdout - - @pytest.mark.parametrize( - "path", + "title,allowed", [ - "README.md", - "tests/test_example.py", - ".github/workflows/ci.yml", - "scripts-other/a.py", + ("patch: Fix it", True), + ("minor: Add it", True), + ("major: Replace it", True), + ("Fix it", False), ], ) -@pytest.mark.parametrize("bump", [False, True]) -def test_excluded_only_changes_forbid_increase(history, path, bump): - repo, base = history - if bump: - version(repo, "1.2.4") - write(repo, path, "documentation\n") - commit(repo) - result = check(repo, base) - assert (result.returncode == 0) is not bump, result.stdout + result.stderr - - -def test_version_only_change_does_not_justify_itself(history): +def test_release_inputs_require_prefix(history, path, title, allowed): repo, base = history - version(repo, "1.2.4") + target = repo / path + write(repo, path, (target.read_text() if target.exists() else "") + "\n# change\n") commit(repo) - result = check(repo, base) - assert result.returncode != 0 - assert "without release changes" in result.stdout + result = check(repo, base, title) + assert (result.returncode == 0) is allowed, result.stdout + result.stderr @pytest.mark.parametrize( - "value,allowed", - [ - ("1.2.4", True), - ("1.3.0", True), - ("2.0.0", True), - ("1.2.5", False), - ("1.4.0", False), - ("3.0.0", False), - ("1.3.1", False), - ("2.1.0", False), - ("2.0.1", False), - ("1.2.4rc1", False), - ("1.2.4.post1", False), - ("1.2.4.dev1", False), - ("1.2.4+local", False), - ("1!1.2.4", False), - ("1.2.4.0", False), - ("v1.2.4", False), - ("01.2.4", False), - ], + "title", + ["patch: Missing release", "minor: Missing release", "major: Missing release"], ) -def test_release_requires_exactly_one_version_increment(history, value, allowed): +def test_nonrelease_changes_forbid_prefix(history, title): repo, base = history - version(repo, value) - write(repo, "src/file.py", "changed\n") + write(repo, "README.md", "docs") commit(repo) - result = check(repo, base) - assert (result.returncode == 0) is allowed, result.stdout + result.stderr - - -@pytest.mark.parametrize("final_version,allowed", [("1.2.4", True), ("1.2.5", False)]) -def test_multiple_pr_commits_share_one_bump(history, final_version, allowed): - repo, base = history - version(repo, "1.2.4") - write(repo, "src/file.py", "first change\n") - commit(repo) - version(repo, final_version) - write(repo, "src/file.py", "second change\n") - commit(repo) - result = check(repo, base) - assert (result.returncode == 0) is allowed, result.stdout + result.stderr + assert check(repo, base, title).returncode != 0 @pytest.mark.parametrize( - "value,lock", [("1.2.2", None), ("1.2.4", "1.2.3"), ("invalid", None)] + "title", + [ + "Patch: Change", + " patch: Change", + "patch:Change", + "patch: Change", + "patch:\tChange", + "patch: ", + "patch : Change", + "major\uff1a Change", + ], ) -def test_invalid_or_inconsistent_versions_fail(history, value, lock): +def test_malformed_release_title_rejected(history, title): repo, base = history - version(repo, value, lock) - write(repo, "src/file.py", "changed\n") + write(repo, "src/file.py", "change") commit(repo) - assert check(repo, base).returncode != 0 + assert check(repo, base, title).returncode != 0 @pytest.mark.parametrize("move", [False, True]) -def test_deleting_or_moving_out_of_release_directory_requires_bump(history, move): +def test_delete_or_move_out_counts_as_release(history, move): repo, base = history if move: - (repo / "src/file.py").rename(repo / "file with spaces.py") + (repo / "src/file.py").rename(repo / "outside.py") else: (repo / "src/file.py").unlink() commit(repo) - result = check(repo, base) - assert result.returncode != 0 - assert "src/file.py" in result.stdout + assert check(repo, base, "Docs").returncode != 0 + assert check(repo, base, "patch: Remove file").returncode == 0 -def test_other_lock_package_version_is_a_release_change(history): - repo, _ = history - with (repo / "uv.lock").open("a", encoding="utf-8") as stream: - stream.write('[[package]]\nname = "other"\nversion = "1.0"\n') +def test_unrelated_main_and_dirty_files_do_not_change_pr_classification(history): + repo, original = history + write(repo, "src/main.py", "main") base = commit(repo) - lock = repo / "uv.lock" - lock.write_text(lock.read_text().replace('version = "1.0"', 'version = "2.0"')) + git(repo, "checkout", "-b", "docs", original) + write(repo, "README.md", "docs") commit(repo) - result = check(repo, base) - assert result.returncode != 0 - assert "uv.lock" in result.stdout + write(repo, "src/file.py", "uncommitted") + assert check(repo, base, "Explain setup").returncode == 0 -def test_stale_release_cannot_reuse_new_main_version(history): - repo, original = history - version(repo, "1.2.4") - write(repo, "src/main.py", "main change\n") - base = commit(repo) - git(repo, "checkout", "-b", "pr", original) - version(repo, "1.2.4") - write(repo, "src/pr.py", "pr change\n") +def test_title_is_data_and_multiple_commits_have_one_intent(history): + repo, base = history + write(repo, "src/file.py", "one") commit(repo) - result = check(repo, base) - assert result.returncode != 0 - assert "must increase" in result.stdout + write(repo, "src/file.py", "two") + commit(repo) + assert check(repo, base, 'patch: $(echo unsafe) `literal` "quoted"').returncode == 0 -def test_unrelated_main_changes_are_not_attributed_to_docs_pr(history): - repo, original = history - write(repo, "src/main.py", "main change\n") - base = commit(repo) - git(repo, "checkout", "-b", "pr", original) - write(repo, "README.md", "docs\n") +def test_manual_version_is_rejected(history): + repo, base = history + write( + repo, "pyproject.toml", '[project]\nname="free-claude-code"\nversion="1.2.3"\n' + ) commit(repo) - result = check(repo, base) - assert result.returncode == 0, result.stdout + result.stderr + assert check(repo, base, "patch: Manual version").returncode != 0 + +def test_pr_snapshot_uses_latest_title_and_rejects_stale_head(history, tmp_path): + import json -def test_checker_reads_commits_not_uncommitted_files(history): repo, base = history - write(repo, "README.md", "docs\n") - commit(repo) - version(repo, "9.9.9") - write(repo, "src/file.py", "uncommitted\n") - result = check(repo, base) - assert result.returncode == 0, result.stdout + result.stderr + write(repo, "src/file.py", "change") + head = commit(repo) + payload = tmp_path / "current-pr.json" + payload.write_text( + json.dumps( + {"head": {"sha": head}, "base": {"sha": base}, "title": "patch: New title"} + ) + ) + result = subprocess.run( + [sys.executable, str(CHECKER), "--head", head, "--pr-json", str(payload)], + cwd=repo, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + payload.write_text( + json.dumps( + {"head": {"sha": base}, "base": {"sha": base}, "title": "patch: New title"} + ) + ) + result = subprocess.run( + [sys.executable, str(CHECKER), "--head", head, "--pr-json", str(payload)], + cwd=repo, + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "head changed" in result.stdout diff --git a/uv.lock b/uv.lock index 489a472d91..95eca9bcb4 100644 --- a/uv.lock +++ b/uv.lock @@ -599,7 +599,6 @@ wheels = [ [[package]] name = "free-claude-code" -version = "6.2.66" source = { editable = "." } dependencies = [ { name = "aiohttp" }, From 67b0a1f0dab5a4568c5a5ebc6713e50beb12def9 Mon Sep 17 00:00:00 2001 From: Ali Khokhar Date: Fri, 25 Sep 2026 14:39:40 -0700 Subject: [PATCH 2/4] patch: Fix post-merge release lookup race (#1904) ## Why Post-merge publication crashed after creating v6.2.67 because GitHub temporarily omitted the new draft from its release list. The same stale list could also report incomplete assets after upload. ## How Retain the numeric release ID returned by draft creation and fetch staging metadata directly by that ID before and after uploading assets. Preserve generated release notes and cover delayed list responses for both new drafts and interrupted uploads. --- scripts/publish_release.py | 42 +++++++++++------ tests/scripts/test_publish_release.py | 66 +++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 24 deletions(-) diff --git a/scripts/publish_release.py b/scripts/publish_release.py index e8d56179c1..e1daf46c0a 100644 --- a/scripts/publish_release.py +++ b/scripts/publish_release.py @@ -40,6 +40,11 @@ def __init__(self, repository: str) -> None: def gh(self, *args: str) -> str: return command(["gh", "release", *args, "--repo", self.repository]) + def release_api(self, suffix: str, *args: str) -> dict: + return json.loads( + command(["gh", "api", f"repos/{self.repository}/releases{suffix}", *args]) + ) + def releases(self) -> dict: pages = json.loads( command( @@ -112,16 +117,23 @@ def publish(self, target: str) -> None: command(["git", "tag", tag, target]) command(["git", "push", "origin", f"refs/tags/{tag}"]) if release is None: - self.gh( - "create", - tag, - "--draft", - "--verify-tag", - "--title", - tag, - "--generate-notes", - "--notes-start-tag", - previous, + notes = self.release_api( + "/generate-notes", + "-f", + f"tag_name={tag}", + "-f", + f"previous_tag_name={previous}", + ) + release = self.release_api( + "", + "-f", + f"tag_name={tag}", + "-f", + f"name={tag}", + "-f", + f"body={notes['body']}", + "-F", + "draft=true", ) print(f"Publishing {tag} from {target}") with tempfile.TemporaryDirectory(prefix="fcc-release-") as temporary: @@ -132,7 +144,7 @@ def publish(self, target: str) -> None: try: if command(["git", "status", "--porcelain"], cwd=source).strip(): raise ValueError("Release source must be clean") - self.stage(tag, version, source, dist) + self.stage(tag, release["id"], version, source, dist) command( [ "uv", @@ -154,9 +166,11 @@ def validate(self, source: Path, dist: Path, version: str) -> None: cwd=source, ) - def stage(self, tag: str, version: str, source: Path, dist: Path) -> None: + def stage( + self, tag: str, release_id: int, version: str, source: Path, dist: Path + ) -> None: names = set(distribution_names(version)) - assets = self.releases()[tag]["assets"] + assets = self.release_api(f"/{release_id}")["assets"] if any(asset["name"] not in names for asset in assets): raise ValueError(f"Unexpected assets on {tag}") complete = ( @@ -173,7 +187,7 @@ def stage(self, tag: str, version: str, source: Path, dist: Path) -> None: command(["uv", "build", "--no-sources", "--out-dir", str(dist)], cwd=source) self.validate(source, dist, version) self.gh("upload", tag, *(str(dist / name) for name in sorted(names))) - assets = self.releases()[tag]["assets"] + assets = self.release_api(f"/{release_id}")["assets"] if {asset["name"] for asset in assets} != names or len(assets) != 2: raise ValueError(f"Incomplete staged distributions for {tag}") for asset in assets: diff --git a/tests/scripts/test_publish_release.py b/tests/scripts/test_publish_release.py index 05f0abd746..51c56a9f32 100644 --- a/tests/scripts/test_publish_release.py +++ b/tests/scripts/test_publish_release.py @@ -2,6 +2,7 @@ import hashlib import json +from itertools import pairwise from pathlib import Path from typing import TypedDict @@ -40,6 +41,7 @@ class Asset(TypedDict): class Release(TypedDict): + id: int tag_name: str draft: bool prerelease: bool @@ -51,6 +53,7 @@ def __init__(self, repo): self.repo = repo self.releases: dict[str, Release] = { "v1.2.3": { + "id": 1, "tag_name": "v1.2.3", "draft": False, "prerelease": False, @@ -73,16 +76,34 @@ def command(self, args, *, cwd=None): return "" return git(Path(cwd or self.repo), *args[1:]) if args[:2] == ["gh", "api"]: - return json.dumps([list(self.releases.values())]) - if args[:3] == ["gh", "release", "create"]: - tag = args[3] - self.releases[tag] = { - "tag_name": tag, - "draft": True, - "prerelease": False, - "assets": [], - } - elif args[:3] == ["gh", "release", "upload"]: + endpoint = next(arg for arg in args if arg.startswith("repos/")) + fields = dict( + field.split("=", 1) + for flag, field in pairwise(args) + if flag in {"-f", "-F"} + ) + if endpoint.endswith("?per_page=100"): + return json.dumps([list(self.releases.values())]) + if endpoint.endswith("/generate-notes"): + assert fields["previous_tag_name"] in self.releases + return json.dumps({"body": "Generated release notes"}) + if endpoint.endswith("/releases"): + assert fields["draft"] == "true" + assert fields["body"] == "Generated release notes" + tag = fields["tag_name"] + self.releases[tag] = { + "id": len(self.releases) + 1, + "tag_name": tag, + "draft": True, + "prerelease": False, + "assets": [], + } + return json.dumps(self.releases[tag]) + release_id = int(endpoint.rsplit("/", 1)[1]) + return json.dumps( + next(r for r in self.releases.values() if r["id"] == release_id) + ) + if args[:3] == ["gh", "release", "upload"]: tag = args[3] for filename in args[4:]: file = Path(filename) @@ -155,6 +176,31 @@ def test_nonrelease_never_allocates_or_builds(releases): assert not any(c[:2] == ["git", "push"] for c in remote.calls) +@pytest.mark.parametrize("existing_draft", [False, True]) +def test_stale_release_list_does_not_block_staging( + releases, monkeypatch, existing_draft +): + _, remote = releases + head = remote.advance() + if existing_draft: + remote.fail = "stage" + with pytest.raises(RuntimeError, match="interrupted staging"): + remote.publisher().publish(head) + listed = json.dumps([list(remote.releases.values())]) + + def stale_listing(args, *, cwd=None): + if args[:2] == ["gh", "api"] and any( + arg.endswith("/releases?per_page=100") for arg in args + ): + return listed + return remote.command(args, cwd=cwd) + + monkeypatch.setattr(publishing, "command", stale_listing) + remote.publisher().publish(head) + assert not remote.releases["v1.2.4"]["draft"] + assert set(remote.uploaded) == set(publishing.distribution_names("1.2.4")) + + @pytest.mark.parametrize("failure", ["stage", "publish", "finalize"]) def test_retries_keep_version_and_published_bytes(releases, failure): repo, remote = releases From 3a0a97f6ed42a7a618262bc6bee66d14033126df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 15:34:42 -0700 Subject: [PATCH 3/4] patch: Update dependencies and fix CI diagnostics (#1902) ## Why Update FCC dependencies and development tools while keeping declared minimum versions aligned with the lockfile. The newer Ty identifies a missing protocol method in the shared catalog test fake. Linux Playwright CI also encountered a native Python crash while its background faulthandler timer was printing thread stacks. ## How Update uvicorn to 0.53.0, openai to 3.18.0, github-copilot-sdk to 1.0.14, grpcio to 1.84.0, packaging to 26.3, Ty to 0.0.83, and Ruff to 0.16.8. Raise the corresponding minimum requirements and configure Dependabot with `versioning-strategy: increase` for future uv updates. Complete the catalog test fake. Keep the 10-second browser-test diagnostics using a Python timer with `sys._current_frames()` and `traceback.print_stack()`. Register the diagnostic plugin centrally, preserve its output across pytest capture, and cancel and join each timer when the test finishes. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alishahryar1 --- .github/dependabot.yml | 1 + .github/workflows/tests.yml | 2 +- conftest.py | 3 + e2e/thread_diagnostics.py | 66 ++++++++++++ pyproject.toml | 14 +-- tests/runtime/test_codex_catalog.py | 4 + tests/test_thread_diagnostics.py | 83 +++++++++++++++ uv.lock | 156 +++++++++++++++------------- 8 files changed, 248 insertions(+), 81 deletions(-) create mode 100644 conftest.py create mode 100644 e2e/thread_diagnostics.py create mode 100644 tests/test_thread_diagnostics.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 776459cfe4..f861019574 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,6 +2,7 @@ version: 2 updates: - package-ecosystem: "uv" directory: "/" + versioning-strategy: increase schedule: interval: "weekly" groups: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 85ec74c7e0..0bc27f3808 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -142,7 +142,7 @@ jobs: - name: Run Playwright run: >- uv run --locked pytest e2e -v --tb=short - -o faulthandler_timeout=10 --durations=20 + -o thread_dump_timeout=10 --durations=20 --tracing=retain-on-failure --screenshot=only-on-failure --full-page-screenshot --output=test-results diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000000..e3e334c176 --- /dev/null +++ b/conftest.py @@ -0,0 +1,3 @@ +"""Shared pytest plugin registration.""" + +pytest_plugins = ["e2e.thread_diagnostics"] diff --git a/e2e/thread_diagnostics.py b/e2e/thread_diagnostics.py new file mode 100644 index 0000000000..a4268102eb --- /dev/null +++ b/e2e/thread_diagnostics.py @@ -0,0 +1,66 @@ +"""Slow-test stack snapshots without the native faulthandler watchdog.""" + +import os +import sys +import threading +import traceback +from collections.abc import Iterator +from contextlib import contextmanager +from typing import TextIO + +import pytest + +_output = pytest.StashKey[TextIO]() + + +@contextmanager +def dump_threads_after( + timeout: float, output: TextIO, test_name: str +) -> Iterator[None]: + def dump() -> None: + print(f"Test still running after {timeout:g}s: {test_name}", file=output) + for thread_id, frame in sys._current_frames().items(): + print(f"\nThread {thread_id}:", file=output) + traceback.print_stack(frame, file=output) + output.flush() + + timer = threading.Timer(timeout, dump) + timer.name = "fcc-test-diagnostics" + timer.daemon = True + timer.start() + try: + yield + finally: + timer.cancel() + timer.join() + + +def pytest_addoption(parser) -> None: + parser.addini("thread_dump_timeout", "Print slow-test thread stacks", default="0") + + +def pytest_configure(config: pytest.Config) -> None: + if float(config.getini("thread_dump_timeout")) > 0: + # Windows xdist workers preserve stderr here and redirect descriptor 2 to NUL. + # Keep a stable output descriptor across pytest's per-test capture changes. + config.stash[_output] = os.fdopen( + os.dup(sys.stderr.fileno()), + "w", + encoding="utf-8", + errors="backslashreplace", + ) + + +def pytest_unconfigure(config: pytest.Config) -> None: + if _output in config.stash: + config.stash[_output].close() + del config.stash[_output] + + +@pytest.hookimpl(wrapper=True) +def pytest_runtest_protocol(item: pytest.Item): + timeout = float(item.config.getini("thread_dump_timeout")) + if timeout <= 0: + return (yield) + with dump_threads_after(timeout, item.config.stash[_output], item.nodeid): + return (yield) diff --git a/pyproject.toml b/pyproject.toml index ee74e01d6c..955c2e89a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ requires-python = ">=3.14.0" dependencies = [ "anyio>=4.15.1", "fastapi[standard]>=0.141.1", - "uvicorn>=0.52.1", + "uvicorn>=0.53.0", "httpx[socks]>=0.28.1", "httpx2[socks]>=2.7.0,<3", "markdown-it-py>=4.2.0", @@ -22,7 +22,7 @@ dependencies = [ "tiktoken>=0.13.0", "python-telegram-bot>=22.8", "discord.py>=2.7.1", - "openai>=3.14.0", + "openai>=3.18.0", "loguru>=0.7.0", "aiohttp>=3.14.3", "jsonschema>=4.25.0", @@ -32,9 +32,9 @@ dependencies = [ "requests[socks]>=2.34.2", "pystray>=0.19.5; sys_platform == 'win32' or sys_platform == 'darwin'", "Pillow>=12.0.0; sys_platform == 'win32' or sys_platform == 'darwin'", - "github-copilot-sdk==1.0.13", + "github-copilot-sdk==1.0.14", "simplejson>=4.1.2", - "grpcio>=1.83.1", + "grpcio>=1.84.0", "grpcio-tools>=1.81.1", "nvidia-riva-client>=2.26.0", ] @@ -87,12 +87,12 @@ explicit = true [dependency-groups] dev = [ - "packaging>=26.0", + "packaging>=26.3", "pytest>=9.1.1", "pytest-asyncio>=1.4.0", "pytest-cov>=7.1.0", - "ty>=0.0.80", - "ruff>=0.16.7", + "ty>=0.0.83", + "ruff>=0.16.8", "pytest-xdist>=3.8.0", "pytest-playwright>=0.9.0", "pyyaml>=6.0.3", diff --git a/tests/runtime/test_codex_catalog.py b/tests/runtime/test_codex_catalog.py index 40a7f93b6b..4a47a8acff 100644 --- a/tests/runtime/test_codex_catalog.py +++ b/tests/runtime/test_codex_catalog.py @@ -13,6 +13,7 @@ ) from free_claude_code.cli.launchers.catalog_http import catalog_models_from_response from free_claude_code.config.settings import Settings +from free_claude_code.core.json_types import JsonObject from free_claude_code.core.model_capabilities import ModelInputModality from free_claude_code.harnesses.codex_model_catalog import build_codex_model_catalog from free_claude_code.runtime.codex_app_server import CodexHarnessFactory @@ -42,6 +43,9 @@ async def acquire( async def wait_for_catalog(self) -> ModelCatalogSnapshot: return ModelCatalogSnapshot(self._settings, self._cached_infos) + def catalog_status(self) -> JsonObject: + raise AssertionError("Catalog publication must not query startup status.") + def current_settings(self) -> Settings: return self._settings diff --git a/tests/test_thread_diagnostics.py b/tests/test_thread_diagnostics.py new file mode 100644 index 0000000000..3b015afa24 --- /dev/null +++ b/tests/test_thread_diagnostics.py @@ -0,0 +1,83 @@ +import io +import os +import subprocess +import sys +import threading +from pathlib import Path + +import pytest + +from e2e.thread_diagnostics import dump_threads_after + + +@pytest.mark.parametrize("workers", ["0", "2"]) +def test_diagnostic_reaches_stderr_with_pytest_capture(tmp_path, workers): + plugin = Path(__file__).resolve().parents[1] / "e2e/thread_diagnostics.py" + (tmp_path / "thread_diagnostics.py").write_text( + plugin.read_text(encoding="utf-8"), encoding="utf-8" + ) + (tmp_path / "pytest.ini").write_text("[pytest]\n", encoding="utf-8") + (tmp_path / "test_slow.py").write_text( + "import time\ndef test_slow():\n time.sleep(0.2)\n", encoding="utf-8" + ) + result = subprocess.run( + [ + sys.executable, + "-m", + "pytest", + "-p", + "xdist.plugin", + "-p", + "thread_diagnostics", + "-n", + workers, + "-o", + "thread_dump_timeout=0.03", + ], + cwd=tmp_path, + env={ + **os.environ, + "PYTEST_ADDOPTS": "", + "PYTEST_PLUGINS": "", + "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", + }, + capture_output=True, + text=True, + timeout=60, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert "test_slow.py::test_slow" in result.stderr + assert "Thread " in result.stderr + + +class DiagnosticOutput(io.StringIO): + def __init__(self): + super().__init__() + self.flushed = threading.Event() + + def flush(self): + self.flushed.set() + + +def test_slow_test_reports_stacks_without_interrupting_execution(): + output = DiagnosticOutput() + with dump_threads_after(0.001, output, "slow-test"): + assert output.flushed.wait(5) + assert "slow-test" in output.getvalue() + assert "test_slow_test_reports_stacks_without_interrupting_execution" in ( + output.getvalue() + ) + assert not any(t.name == "fcc-test-diagnostics" for t in threading.enumerate()) + + +@pytest.mark.parametrize("fails", [False, True]) +def test_completion_cancels_and_joins_pending_diagnostic(fails): + output = DiagnosticOutput() + try: + with dump_threads_after(60, output, "completed-test"): + if fails: + raise RuntimeError("original failure") + except RuntimeError as error: + assert fails and str(error) == "original failure" + assert output.getvalue() == "" + assert not any(t.name == "fcc-test-diagnostics" for t in threading.enumerate()) diff --git a/uv.lock b/uv.lock index 95eca9bcb4..ef50412694 100644 --- a/uv.lock +++ b/uv.lock @@ -657,9 +657,9 @@ requires-dist = [ { name = "anyio", specifier = ">=4.15.1" }, { name = "discord-py", specifier = ">=2.7.1" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.141.1" }, - { name = "github-copilot-sdk", specifier = "==1.0.13" }, + { name = "github-copilot-sdk", specifier = "==1.0.14" }, { name = "google-auth", extras = ["requests"], specifier = ">=2.56.3" }, - { name = "grpcio", specifier = ">=1.83.1" }, + { name = "grpcio", specifier = ">=1.84.0" }, { name = "grpcio-tools", specifier = ">=1.81.1" }, { name = "httpx", extras = ["socks"], specifier = ">=0.28.1" }, { name = "httpx2", extras = ["socks"], specifier = ">=2.7.0,<3" }, @@ -669,7 +669,7 @@ requires-dist = [ { name = "loguru", specifier = ">=0.7.0" }, { name = "markdown-it-py", specifier = ">=4.2.0" }, { name = "nvidia-riva-client", specifier = ">=2.26.0" }, - { name = "openai", specifier = ">=3.14.0" }, + { name = "openai", specifier = ">=3.18.0" }, { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'win32'", specifier = ">=12.0.0" }, { name = "pydantic", specifier = ">=2.13.5" }, { name = "pystray", marker = "sys_platform == 'darwin' or sys_platform == 'win32'", specifier = ">=0.19.5" }, @@ -681,21 +681,21 @@ requires-dist = [ { name = "tomlkit", specifier = ">=0.13.3" }, { name = "torch", marker = "extra == 'voice-local'", specifier = ">=2.14.0", index = "https://download.pytorch.org/whl/cu130" }, { name = "transformers", marker = "extra == 'voice-local'", specifier = ">=5.17.0" }, - { name = "uvicorn", specifier = ">=0.52.1" }, + { name = "uvicorn", specifier = ">=0.53.0" }, ] provides-extras = ["voice-local"] [package.metadata.requires-dev] dev = [ - { name = "packaging", specifier = ">=26.0" }, + { name = "packaging", specifier = ">=26.3" }, { name = "pytest", specifier = ">=9.1.1" }, { name = "pytest-asyncio", specifier = ">=1.4.0" }, { name = "pytest-cov", specifier = ">=7.1.0" }, { name = "pytest-playwright", specifier = ">=0.9.0" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "pyyaml", specifier = ">=6.0.3" }, - { name = "ruff", specifier = ">=0.16.7" }, - { name = "ty", specifier = ">=0.0.80" }, + { name = "ruff", specifier = ">=0.16.8" }, + { name = "ty", specifier = ">=0.0.83" }, ] [[package]] @@ -750,7 +750,7 @@ wheels = [ [[package]] name = "github-copilot-sdk" -version = "1.0.13" +version = "1.0.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -758,7 +758,7 @@ dependencies = [ { name = "python-dateutil" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/6d/4f2e3dddc576ff49739317adba6f9b8c35e93b835ece923e46f1b7fa6028/github_copilot_sdk-1.0.13-py3-none-any.whl", hash = "sha256:941dd5b55cf32ba55c73c651052a4a52b259b470c68bf6a6ac3d240c235402c9", size = 588198, upload-time = "2026-09-04T21:31:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e2/4d8c622545f55f06773548cf86ffe60f59e6a4cedcc412ff84fcac244b2b/github_copilot_sdk-1.0.14-py3-none-any.whl", hash = "sha256:9f4538cec7295f24507c650d794307396458a51f280a0fbbcd840da177109050", size = 614607, upload-time = "2026-09-16T05:11:14.572Z" }, ] [[package]] @@ -828,23 +828,33 @@ wheels = [ [[package]] name = "grpcio" -version = "1.83.1" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/b1/46539f5050d7c316a13396d185451f95084a74ddc68b12d818595bef0377/grpcio-1.83.1.tar.gz", hash = "sha256:9cee6fcbf2eb57c4b49451787bfa87be8efc1ca02a0b327dd4b54d44502e362b", size = 13445033, upload-time = "2026-08-28T07:09:11.464Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/9c/484d981d8b90c4e6abf3030bd2ed747e84d1eb192b3ec9cbb41e0b73e4bf/grpcio-1.83.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:8b3c87ca908296bf125f841d3e1a2225a2b39aaa8ed7a57e7ccde465ee519bab", size = 6306089, upload-time = "2026-08-28T07:08:48.379Z" }, - { url = "https://files.pythonhosted.org/packages/84/01/0afec1c92e4f292f74a44ecf75eabbf40903125b8c4df103c9868d6338da/grpcio-1.83.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c0f3f20c90e72a171917ae65706500b096a1c3eb5f162c3ce702a2e25635f132", size = 12170381, upload-time = "2026-08-28T07:08:50.653Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5a/e9a2383804433a0a61d6d93777ad321c7f36ac1cfdaa4c6d1a7c9ac846b7/grpcio-1.83.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81bbf35a46bf8cad2dfbb2eccc19c711befb58b288acb534bbcd0d74283202a6", size = 6883286, upload-time = "2026-08-28T07:08:53.654Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/f8ca8f76994e14c70b9a0052e82f10de497a23db450c36379c9716ebfc4d/grpcio-1.83.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:215cec07d11176507387bda4bf2751816e880f9bff8dc1ca524bfbb8ed8f2fad", size = 7624293, upload-time = "2026-08-28T07:08:55.709Z" }, - { url = "https://files.pythonhosted.org/packages/bb/7a/4b672814b0cd0fe63bdd735379d88b165759f3144ab023ad8ec5fc4d53ac/grpcio-1.83.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:abce7d43ec29cd39230fa8339de1a07643b55adc412a454850fbd875349950ff", size = 7044346, upload-time = "2026-08-28T07:08:57.802Z" }, - { url = "https://files.pythonhosted.org/packages/50/b8/d89fe60e4239ad51be333dd9cc703741d449a35064e51f8a0b5bfa755432/grpcio-1.83.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e256f95a40e3b0183a98556fb7164d24b97eeb353123ccabfcba94712b35ee2a", size = 7584187, upload-time = "2026-08-28T07:08:59.867Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b2/b290d7402633d9166e4dd47e6f5f74a24ce10a8340b84455896ebc349f85/grpcio-1.83.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2110059146fb0ea216e1ffddb29377b5cc2fd412a5b0a92e102616bd5edf18c2", size = 8608730, upload-time = "2026-08-28T07:09:02.592Z" }, - { url = "https://files.pythonhosted.org/packages/f5/44/fa89e44d1b5cf5b9fa71b2fd7abf506f182fd43917231a92fbf1ea326b02/grpcio-1.83.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20d944d967843f8183f9f23d5916388362e5f8eeeae855bbe4354d906dc9f31b", size = 7983283, upload-time = "2026-08-28T07:09:05.087Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b8/9db73ed1f35ffa76124ac574bf296d06a359798dfd6b50d382f2b8a060a1/grpcio-1.83.1-cp314-cp314-win32.whl", hash = "sha256:623c87c6d4a1cb30d82c4e896f95477050f2e01b4a1f8cf91ff2b1abdf89c457", size = 4474327, upload-time = "2026-08-28T07:09:07.179Z" }, - { url = "https://files.pythonhosted.org/packages/65/22/fc9a622d885a7a37ff972a12faaef443d74e47407181da70d0ab62ab41f0/grpcio-1.83.1-cp314-cp314-win_amd64.whl", hash = "sha256:47e6934ad38779271e2e7cc5f78a63a407cf3d98114c65c1fdbcd3f5a716f29b", size = 5302032, upload-time = "2026-08-28T07:09:09.285Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/6f/e25ca89ca5b0b7b95464c907a5c21a77c0ac8c4ee1dca164c4dd8f153ddb/grpcio-1.84.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:026d757df86c5b7a41de8200b9a2cda454aaa5004cb0c7e3374c66eb82f61499", size = 6428207, upload-time = "2026-09-14T06:58:34.401Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b4/6b76b429f3f9b901cdbc306c81364d708bc957f847a05cbd1046cd2d05d8/grpcio-1.84.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3de427b05f244ba2c2a9bdc67e7a6731c8340811524ecc4435466549f8af1d17", size = 12342420, upload-time = "2026-09-14T06:58:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/ac86d638ba7f73bee0dccb608ba551d4f63adf75151f00d2c43e46d3979e/grpcio-1.84.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e90e3bdf7b5eac005fef631adae9cafde16f922def207b80a7c46b253c18ad20", size = 6998396, upload-time = "2026-09-14T06:58:40.535Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/fa12e9ec9d7ebf8cc3e81428fa9e1ca0d30d22d546ce2baa4c64bc917cbc/grpcio-1.84.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e88d304f094f4937bc27ec6a435e218a084168f11ec630c8d5d39b431d08d81d", size = 7757538, upload-time = "2026-09-14T06:58:43.297Z" }, + { url = "https://files.pythonhosted.org/packages/21/d7/94240c7fae121ff1f116dcf04a3b7ee0216a06832c704310363f72638d4c/grpcio-1.84.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57dc36a5ab0e676f5f6e171de2917fd0aef73f32a9aaf23956bfe19997a30bd1", size = 7161480, upload-time = "2026-09-14T06:58:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/c9/7033e95d4b344969818b09185721c7608b47fc2498d97b5e4eec4995dbf3/grpcio-1.84.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5deda5b4bf62769eb98c119cca43d40e1231e34846b19db5cdea821d446a2253", size = 7720191, upload-time = "2026-09-14T06:58:48.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/b45df2deba81d55069076859480bae7109c9eec02bce5515c799530cc2aa/grpcio-1.84.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9bab4cf571653a8afffb83ce21aa27b51dfe629b526b7b6adec35491fe1fc2ea", size = 8762792, upload-time = "2026-09-14T06:58:51.068Z" }, + { url = "https://files.pythonhosted.org/packages/de/c4/3e1c3d6155c16b8737cc31d5b477d6cf1fc7cdd10d58320cf0ec9b446f42/grpcio-1.84.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5559b492007dc09b4de9b95dab05f0b5e53547aad230cf07e46c7dd017a3be5", size = 8123299, upload-time = "2026-09-14T06:58:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/f4864de5b815e5ba18858771f99381a398fac14117f89ef5291ed43d3c4e/grpcio-1.84.0-cp314-cp314-win32.whl", hash = "sha256:2c024da73b296f040b8360e60bd73a659b230093684a438da0e1260f34cc724e", size = 4562560, upload-time = "2026-09-14T06:58:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/44/03/640811d4d8c84f5e603995c5a9bab725223aa472cad9ca4286c3bbf1c3e3/grpcio-1.84.0-cp314-cp314-win_amd64.whl", hash = "sha256:800b7e00d92553313c0463c200087930aa78678ec1d528193aeb50906f55989b", size = 5394092, upload-time = "2026-09-14T06:58:59.61Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1a/9e3d2c9f005f680f03308fa894b1db91d4ab3f0fe65ff630c69561e91e95/grpcio-1.84.0-cp315-cp315-linux_armv7l.whl", hash = "sha256:47ecf0d9b81d981f07b61bd89eced9d2582f5eaacc3aaa36ad27f81aef70a27f", size = 6428252, upload-time = "2026-09-14T06:59:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/77/34/0bc9f52ebf091311651eeab3a452fb557985604a3088cb5406f4d6df85d3/grpcio-1.84.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:61386101ecaa096b694d0dd278caf99a56aeec78440cc17e918eef0b50f2d567", size = 12359488, upload-time = "2026-09-14T06:59:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/c31052712f241cb6ecae9c226fabd519b7f8c64a7a40bac27e9ca0405b78/grpcio-1.84.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6d178ba6dc8e82976c184b65fddde172d054c17237993a3e083efe4f134d55b", size = 7019339, upload-time = "2026-09-14T06:59:08.76Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/b9b33ea4f1eb4cad28833cade604febf357385b5ebb0c9c7562d020e167a/grpcio-1.84.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:15bb76489e337fc492685c9758e2fd4d4ab516b901ad830dc5a91987decf00be", size = 7107974, upload-time = "2026-09-14T06:59:11.568Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9e/799d4c45db91bbdcd8c54b3982932dbcf3d059f7ce67dca3e8540faa1ece/grpcio-1.84.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82da34ae4f639c73ac46e521e00c0a49bf86f717b9fb1f405f133e98731e38dc", size = 7200036, upload-time = "2026-09-14T06:59:14.401Z" }, + { url = "https://files.pythonhosted.org/packages/45/dc/dcfdd13ada41aff9098f0c2c6f260eb7debbc88b84b7e5fcbd085165427d/grpcio-1.84.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9b73836ba0e16fcbb57c31cf6cbc2907c8d8c790b83679df454b74bd15e0be04", size = 7742281, upload-time = "2026-09-14T06:59:17.348Z" }, + { url = "https://files.pythonhosted.org/packages/55/31/75eab2ec77b80804bc5e21cec99b57598e726fca6484cd3e8920a97639d5/grpcio-1.84.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:42959bd50dd660ffc3f2a9bec15a6da4f9aaa0dda555d59ff2d2e80b908456a8", size = 8113629, upload-time = "2026-09-14T06:59:20.584Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/fdcf6bdc1df9ca11679a1187bef8e6b81df31a2baae69497e17344f05ea3/grpcio-1.84.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:659728f20fc7a0933ed7b1945435e31014b97ab8a5a7edcbaa70da4794aeb191", size = 8152972, upload-time = "2026-09-14T06:59:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cf/6720e720bfa80fcb1ace873f66724eb3c8b03bba2fa078a30c12cab3212e/grpcio-1.84.0-cp315-cp315-win32.whl", hash = "sha256:edb6f87fc60ff438557291501b3e16c7a77c3b01a52d782cf276dccc7c5dd89c", size = 4561981, upload-time = "2026-09-14T06:59:27.275Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/69d8a709df225bc2e06e028e9465166b174c24b3da07cc72d9a5ddc63194/grpcio-1.84.0-cp315-cp315-win_amd64.whl", hash = "sha256:4119efa6519871719ad81f33bc95ab87857dcb1c5801f30a6e592f2c41164169", size = 5394757, upload-time = "2026-09-14T06:59:30.118Z" }, ] [[package]] @@ -1545,7 +1555,7 @@ wheels = [ [[package]] name = "openai" -version = "3.14.0" +version = "3.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1555,18 +1565,18 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/88/472211fbaee888238d2e0657bcfdba5f64f3d38899dbfdada5e632a9758f/openai-3.14.0.tar.gz", hash = "sha256:714ba70b91ee1e78e75263694bebfeaca004432032685333a46d0e0206114bf9", size = 1635622, upload-time = "2026-09-14T23:29:10.961Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7b/a960e698f7126f31113764d7b441d7fe73caada32b6e65c6ca42c5343265/openai-3.18.0.tar.gz", hash = "sha256:780946991bc825f110ddde12c8c84885dd012ae64143d56d494922b7bb879b78", size = 1711178, upload-time = "2026-09-22T18:26:31.81Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/e9/53504fbb21b2b765f77f972d34f86a80e5b98ec274b503a8e3d557df2aac/openai-3.14.0-py3-none-any.whl", hash = "sha256:232a85a1c0ff6820534630fbfdb99313990ed80e66f39892a3abcff4130e2b16", size = 2010880, upload-time = "2026-09-14T23:29:08.978Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c3/262e12d6dc544e215c3f4f76525b0fadf749f8092e525772b9f868d89ea9/openai-3.18.0-py3-none-any.whl", hash = "sha256:31f170b59865cbf35ceb15699bd33b190fa660eb2f36591c74a161afc67152e7", size = 2068693, upload-time = "2026-09-22T18:26:29.518Z" }, ] [[package]] name = "packaging" -version = "26.0" +version = "26.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] @@ -2304,27 +2314,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/bb/5a449b9162e49b139d72f61672bd3ac1d790221f796d3304e2241fff4c58/ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6", size = 4924184, upload-time = "2026-09-10T18:04:06.336Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/b2/c80aeeb7f9e469c0d63a85d2f1ab6e1ebfbe10ea7a8d2438b7e09e3ff09e/ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9", size = 10048917, upload-time = "2026-09-10T18:03:30.28Z" }, - { url = "https://files.pythonhosted.org/packages/7b/96/20bb7bcae008004df52afcb7ac83432d4a467f2c17b672fe46d26be231c5/ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929", size = 10242929, upload-time = "2026-09-10T18:03:32.706Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f184b0d5abec02db69cfd7e49b688ae0237554528ca777136c613bf36bee/ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728", size = 9847245, upload-time = "2026-09-10T18:03:34.509Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2d/db1633a641866ed801e34cc6b60ef236c5e16f9b2124ab1d49cc24a5fe4f/ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0", size = 9961780, upload-time = "2026-09-10T18:03:36.618Z" }, - { url = "https://files.pythonhosted.org/packages/4d/98/edea21e1a3e38dbbc3bf6bb068b863b3b06184cf8533a4c7dbbe208a89d5/ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0", size = 9866337, upload-time = "2026-09-10T18:03:38.805Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/a15e60d4c87b214646f116ca9d204475bf993ee1047459bc9a360fd4d6d1/ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9", size = 10562512, upload-time = "2026-09-10T18:03:40.71Z" }, - { url = "https://files.pythonhosted.org/packages/29/42/eaff4c9b6d0c7cdf56df313a17e89ae854f5bbc0b0c8f9cce19be0ab7a8f/ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1", size = 11302938, upload-time = "2026-09-10T18:03:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/c75aa59a4ec181fe2ec06cab30e198c1c6d107229a9f008ae3a7c16cabd8/ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9", size = 10840857, upload-time = "2026-09-10T18:03:44.604Z" }, - { url = "https://files.pythonhosted.org/packages/21/33/81f3da371942ea031105ba679d8d6e28ec1660ccd690a45f42d381161356/ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55", size = 10370001, upload-time = "2026-09-10T18:03:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/fa/0b/6345fb4dbf6dd0ed1cfe5d18391dc9c3f59cc81622a7b0a65b84b3e730ba/ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f", size = 10548735, upload-time = "2026-09-10T18:03:48.658Z" }, - { url = "https://files.pythonhosted.org/packages/3f/4d/c5576adf511f92a328e5569dda190ecdd430da51f1a649f3a4a2fd73e21e/ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656", size = 10108496, upload-time = "2026-09-10T18:03:50.563Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8c/667d83c16199a17a56adc6b0bd4c3beb5b767a2babcd16a56f76f9be7fd6/ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750", size = 9860136, upload-time = "2026-09-10T18:03:52.621Z" }, - { url = "https://files.pythonhosted.org/packages/99/75/78d401106731999a1dd20cc5a6961e37e1eb9397a3b589f73f3a5ce146a3/ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5", size = 10286290, upload-time = "2026-09-10T18:03:55.207Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/56f9c3a8b755df93a0ad318b2147bf4ef5dae9a7e5ec61c460109c67957f/ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f", size = 10745048, upload-time = "2026-09-10T18:03:57.299Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ea/7f9b938a63ece4bec677ad7f9f7fa02df3383db1949ed93a382441c09a87/ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7", size = 10059082, upload-time = "2026-09-10T18:03:59.632Z" }, - { url = "https://files.pythonhosted.org/packages/39/11/480a6973a927aa653e1cead6a6416008640e03a99d05b34c0434b8c6c366/ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6", size = 10593368, upload-time = "2026-09-10T18:04:01.999Z" }, - { url = "https://files.pythonhosted.org/packages/8b/4b/51327018d056f0dad2c2238f26d1fb0f53707a9d91b75dea6d1b3039f136/ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b", size = 10412401, upload-time = "2026-09-10T18:04:04.098Z" }, +version = "0.16.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, ] [[package]] @@ -2744,27 +2754,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.80" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/b0/6d1b10e0d422736a3c439e487c950ae785f401d71ff879d5be68bccb6d90/ty-0.0.80.tar.gz", hash = "sha256:fe86bc91327e45ff5e3593b7e306e7f57a44bc0608f38d647b8d97bd99c96013", size = 7183998, upload-time = "2026-09-09T21:18:47.547Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/c8/3c93195eca282936ebb574d0ba98843e4b147ee324006f88464777012a92/ty-0.0.80-py3-none-linux_armv6l.whl", hash = "sha256:738d1cfca466c577aea24547c348629c00c63548cf7da2c46b497b3840f85dee", size = 13606509, upload-time = "2026-09-09T21:18:10.476Z" }, - { url = "https://files.pythonhosted.org/packages/e7/48/bbb47f7001c97262a5109bcd92a45bcc8a2fbb21e994c214a9897630bfb7/ty-0.0.80-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:56060164bb8ee43770fa367524fbdba2611fc90f8923a02cc91f80eb37d5a1b0", size = 13203812, upload-time = "2026-09-09T21:18:12.796Z" }, - { url = "https://files.pythonhosted.org/packages/db/8d/fd141160567047b742e466be8ed09a4c4ed80fa045d37f946eb4f4532fee/ty-0.0.80-py3-none-macosx_11_0_arm64.whl", hash = "sha256:da4062e0fbf3923d9b71688157c5753394f243348f00732e9edbb27498397169", size = 13021896, upload-time = "2026-09-09T21:18:15.186Z" }, - { url = "https://files.pythonhosted.org/packages/1e/10/b4177faf9e71bc37bc4d08f512042269660a1ce0ec457c48f96e51fc91d7/ty-0.0.80-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9eb94b2659f506a3a07a9ea1415d5c18aaa1e554adc1612614d617a0ed320e1", size = 13083881, upload-time = "2026-09-09T21:18:17.616Z" }, - { url = "https://files.pythonhosted.org/packages/7f/c2/192d48b9a9acbbe030fc9a4bd73af75671a5566e731949f77e912a68ce68/ty-0.0.80-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2f5e39a87da48c1af1a3b3135f02be7a83a93da30b0d6a5f5d27806e7c747ce", size = 13354874, upload-time = "2026-09-09T21:18:19.744Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5b/736dfd31efd98bed9dbe018ff91676e5ea83485b1e24afb635fd0247728f/ty-0.0.80-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e00149e7779c6b98f3ba12e10a631a861bd7ca1a42bbf064e2677ada7d2c0c2", size = 14204805, upload-time = "2026-09-09T21:18:21.999Z" }, - { url = "https://files.pythonhosted.org/packages/fd/6f/557c726cb53401998e3589bb632c963a6887a70240e0a4386024f3731c81/ty-0.0.80-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a377268f359fb6e7a2cc9026a09223c764f58d020dec8e6baeaba9caba6ff829", size = 14630014, upload-time = "2026-09-09T21:18:24Z" }, - { url = "https://files.pythonhosted.org/packages/e9/19/7a4b18fe27f6b6bd4ffa56b67c8b09ecb76bb4312d1c64b73f65417b4dbd/ty-0.0.80-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4511dbf1b266b9ce5b73e9b28d94468096c2b6da00e2fe205168c32d3b352ca7", size = 14326946, upload-time = "2026-09-09T21:18:26.679Z" }, - { url = "https://files.pythonhosted.org/packages/f8/95/16dd90805fc7e53ec54ae9fdb1a8b74753fb159a55a170f60f49b9417824/ty-0.0.80-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe95feffa7156800c6f804195acb9fb5846a39671c7192c30fff23351aaf7c31", size = 13705988, upload-time = "2026-09-09T21:18:28.681Z" }, - { url = "https://files.pythonhosted.org/packages/b1/09/4e87992c23ab8a742c7d4914ac5fe66fb9301c8464a69fd2ecbc0be3fbcd/ty-0.0.80-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ba26b39f06bc8c3c2c5acd3a147239482b36620cdbca1b9d02e915294854f2cc", size = 14233729, upload-time = "2026-09-09T21:18:30.961Z" }, - { url = "https://files.pythonhosted.org/packages/36/e9/b8c11fda8e66a1d1cc1a7160ed719a33f4dc0107b1cef6a14e2673965763/ty-0.0.80-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2e6167320888c115a6fbe69893b63fd1c848f9121a454e75e5f612674528e3f3", size = 13173523, upload-time = "2026-09-09T21:18:33.266Z" }, - { url = "https://files.pythonhosted.org/packages/73/a7/512083fa540c5be1ac8642658f03bfb5bfa6fd168ea4f55e3c0aabe04166/ty-0.0.80-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7aecb62b4de70b479eab07d1779ea66da26dd8b068f50a9330867157312f103c", size = 13373014, upload-time = "2026-09-09T21:18:35.339Z" }, - { url = "https://files.pythonhosted.org/packages/5e/63/cd2f0ca81fd9b8cafef93bbcf022bf636bc4de8ee7465c6aafeec1d4d52b/ty-0.0.80-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4fed06adacd16b7e2d722f37449021b1419598d0440769701c0f4827faddde63", size = 13667827, upload-time = "2026-09-09T21:18:37.241Z" }, - { url = "https://files.pythonhosted.org/packages/0d/5d/ebdfdcb2dc099ae74dc4b75511eef0dd398b2fa27006c543543974b4567f/ty-0.0.80-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:09329af6303ce611ec2cfffc995dc1ddf76c9f47194fc1a4d64a984759c25f5b", size = 13963866, upload-time = "2026-09-09T21:18:39.386Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/c286d5e1b560cfd16d6e91fdab683718aacf09fc88938ca06dcc9d1c8502/ty-0.0.80-py3-none-win32.whl", hash = "sha256:a81b3b512f7b4c68e42fbd1835633de658809666e9aafd5defa52bbc5197698d", size = 12881806, upload-time = "2026-09-09T21:18:41.507Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c8/3b3a8ac16d47a23ff34bf57c0d99a491860748aea96e0e886b4bfed54f9b/ty-0.0.80-py3-none-win_amd64.whl", hash = "sha256:8043f99878a2ae434781cb881c8a3840dff70c4a5b60bdc5ae84251f35ee063f", size = 13528883, upload-time = "2026-09-09T21:18:43.687Z" }, - { url = "https://files.pythonhosted.org/packages/2e/71/a6c697930fca76596d7d17f8853120f41a298fd9ea632c901cfca8ca869b/ty-0.0.80-py3-none-win_arm64.whl", hash = "sha256:e277ef034331da5319efc839c064968d24f35715b71c70369cf97d36fe4dcbdb", size = 13370135, upload-time = "2026-09-09T21:18:45.758Z" }, +version = "0.0.83" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/7d/2fd9575bce2d14e2281bec82d0713dee68329b0dc944e9c06c2e36b761fe/ty-0.0.83.tar.gz", hash = "sha256:db118de73c05ac476faceb4d42d59782feaeddfc4d721fd3ae8b807a0a2ae4e4", size = 7373076, upload-time = "2026-09-21T22:47:29.207Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/85/7259431bade2317d8b82fa27220ae4618c21396acf0ca28a7bd75e92afd7/ty-0.0.83-py3-none-linux_armv6l.whl", hash = "sha256:836bc08bad96ecc7e38baf1a6ddaec15aa8c8b46ae99a96365397b5f6481b7a5", size = 13905669, upload-time = "2026-09-21T22:46:36.677Z" }, + { url = "https://files.pythonhosted.org/packages/d2/30/9527386ade8dd71fc52f65cde3c094411af7ad0d505f9063ef0841998702/ty-0.0.83-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f1fb029fe076c7dca404d98e38fdb5fdbc2038a09407f89d43be1fdd9b2c459", size = 13499505, upload-time = "2026-09-21T22:46:40.114Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/63080d8cfd59a53810499a69a2ebc995724be27618b70d7f099e73ace317/ty-0.0.83-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3515776d01ee3b1be974dafa73d449d0bc711afbc12ba341f82f70bc641ff4ee", size = 13498980, upload-time = "2026-09-21T22:46:43.197Z" }, + { url = "https://files.pythonhosted.org/packages/49/78/e647f190bd9513d09db30d45309e9b5df070b5caa44af2de3290fab59fbe/ty-0.0.83-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5b80d66c7f9c8d60f25ccfd40c6eaebe8fd7c9005074904e0e164dd89c13d03", size = 13494705, upload-time = "2026-09-21T22:46:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/2e/12/aef645b13c5bd15e3e5993537dbc23f7d65475f3cf1755d07b4d3e56850d/ty-0.0.83-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57eae4aa55dab1af5d1423227c6517ccab1e658fe265c8e5a58caa584ca2876b", size = 13661466, upload-time = "2026-09-21T22:46:49.392Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c4/11872e823653c568468ef7f0c3b23aed7ad2d33652e20144d28d5bfad58a/ty-0.0.83-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dea3680911340ecf93dff5786dbeefd69c9399df685a22751e1fbd27e6007b50", size = 14479677, upload-time = "2026-09-21T22:46:52.277Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/5b0d0d24d16d24d781f29ee842ee718903e453dc4068acb02bef860d5540/ty-0.0.83-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8453786c274f48e2a89b78e8a505a4f449323f357f2ce26a9b90a4bd68176f67", size = 14972208, upload-time = "2026-09-21T22:46:55.242Z" }, + { url = "https://files.pythonhosted.org/packages/54/04/0077a53bcf6ceb41365a45d8091b52094c91a5f4b071c2a6d705ed8bb65b/ty-0.0.83-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7701bdae3b52271cf0835abe4202aa3b23b735425d8c5a3e6a8ca1c68d39ead8", size = 14660550, upload-time = "2026-09-21T22:46:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ff/b6c879f1c3121387edd19ebdb1fb02af30691f88f632278a5d467fb5f348/ty-0.0.83-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa95b4376447e5309fe3ccc98ee75916a9082714e6a029b99427ca62021dc94a", size = 14147376, upload-time = "2026-09-21T22:47:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bc/82e730247d53cb02acd5a91237c0c62f215b2e0379689a550176d307ed89/ty-0.0.83-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2a4550a08fc1899a33e4a08787a73ec9daa99d292268df4f0a5f1c24c63854c3", size = 14567502, upload-time = "2026-09-21T22:47:04.344Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/bb6f57923231f51b626931625ada71e219d64541f8fadf96065bc5034358/ty-0.0.83-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:feb72080f0835fa2b27485e3b11fa50dc57c30e0ba73808a3f7d64b56db83f6f", size = 13443051, upload-time = "2026-09-21T22:47:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/50/20/9862431662d4453527cd52cf0f30d5975cd1801aa39e570bf45fb299be77/ty-0.0.83-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3e5206def2d6e1df93c75ed8dc763255233ccc3153647509d125bada10ad770a", size = 13678908, upload-time = "2026-09-21T22:47:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e3/94ebbe1573b76660fa09f2dbce96da6d0bb69a2d29c7b6ad538b452979d4/ty-0.0.83-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bdf5dfa157f5c127ba096fe0561b47877e6e79562c7fc2b35906ba5e9f6bb723", size = 13937248, upload-time = "2026-09-21T22:47:14.133Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c0/3ea45c2ec3a82fddb7137e457ccb2a452446905430d225adaa4990cdc338/ty-0.0.83-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:626d6ad00f05a5d9b7ea80de44b12931d0c0fb50e64eab20b7fcc13e9aff2a68", size = 14290826, upload-time = "2026-09-21T22:47:16.874Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b2/c941ce83a59e46ef31dd04bdf6067530e7ec77d5f0cdd1e223fbc6fc351b/ty-0.0.83-py3-none-win32.whl", hash = "sha256:0391017dac6ba3e452e0322301f50bba12fbccadac07d7fd888c46951ad37638", size = 13105291, upload-time = "2026-09-21T22:47:19.807Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8a/5eef9458505698b144643cbd2062f465c5d860d8cf86a8710d74505ad9a0/ty-0.0.83-py3-none-win_amd64.whl", hash = "sha256:1b67d86f5bbad4076ae1a2ff23a6a4493219885d72d4b148cb0193e090390060", size = 13901557, upload-time = "2026-09-21T22:47:23.429Z" }, + { url = "https://files.pythonhosted.org/packages/9f/27/a06061513f1de25569c2e8898018bd552ab7b31afa7cfe818703269b3350/ty-0.0.83-py3-none-win_arm64.whl", hash = "sha256:a916a3d7d68fe30b7aacdb9fe16d461fa944d722e59e96ea0fa9d206a1b6625f", size = 13627960, upload-time = "2026-09-21T22:47:26.305Z" }, ] [[package]] @@ -2814,15 +2824,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.52.4" +version = "0.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/ad/04bbb797c84fc1f26cb171f7394716f4865ffb8d8c5e1eef42565c2dfa6b/uvicorn-0.53.0.tar.gz", hash = "sha256:a9356f0cb89b3b8621529c5d5eebd69bfe154f4c3f68b4cf2de47e45fa855c2e", size = 110881, upload-time = "2026-09-14T07:44:23.815Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/0eea75741ee812e9f598b687619ce2454f6c3a1c5cd21ea990ec6bd26f45/uvicorn-0.53.0-py3-none-any.whl", hash = "sha256:e8dca71ec86dce5f04e333f0d56cdedf942446e6643b9cea1af0d6d3a02cb03e", size = 87081, upload-time = "2026-09-14T07:44:22.179Z" }, ] [package.optional-dependencies] From 8bb45098e13651822c6940cc2557ea656fae53c8 Mon Sep 17 00:00:00 2001 From: Ali Khokhar Date: Fri, 25 Sep 2026 18:40:29 -0700 Subject: [PATCH 4/4] patch: Require Python 3.14.7 and remove .python-version (#1905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Require Python 3.14.7 and remove the separate `.python-version` file so project and CI Python selection come from `pyproject.toml`. ## How Set `requires-python` to `==3.14.7` and regenerate the lockfile. Derive CI installation, cache keys, and the release interpreter from that requirement. Update both installers to request 3.14.7 and enforce agreement between their pins and the project requirement.

RetriggerConfidence Score: 4/5

Not safe to merge: CI can stop before installing the required Python.

Fix All in CodexFindings

1. P1 **Host Python blocks CI setup** ▶
Summary The PR pins the project to Python 3.14.7 and derives CI interpreter selection from pyproject.toml. Contributor setup instructions and CI parser coverage are updated. An existing CI setup issue remains outstanding.
Reviews (3) · Last reviewed commit: ["Wait for integration state before assert..."](https://github.com/alishahryar1/free-claude-code/commit/dd75201b2dfd3e0187b347569c276412ef74d3ec) --- .github/actions/ci-environment/action.yml | 7 +- .github/workflows/post-merge.yml | 4 +- .python-version | 1 - CONTRIBUTING.md | 5 +- e2e/test_progressive_startup.py | 11 ++- pyproject.toml | 2 +- scripts/install.ps1 | 2 +- scripts/install.sh | 2 +- tests/contracts/test_uv_policy.py | 74 +++++++++++++++ tests/installers/test_installers.py | 6 +- uv.lock | 106 +--------------------- 11 files changed, 104 insertions(+), 116 deletions(-) delete mode 100644 .python-version diff --git a/.github/actions/ci-environment/action.yml b/.github/actions/ci-environment/action.yml index 438865ddd7..2cd32c7d2d 100644 --- a/.github/actions/ci-environment/action.yml +++ b/.github/actions/ci-environment/action.yml @@ -19,6 +19,9 @@ inputs: default: "false" outputs: + python-version: + description: Exact Python version required by the project + value: ${{ steps.identity.outputs.python-version }} python-path: description: Managed Python cache directory value: ${{ steps.identity.outputs.python-path }} @@ -62,8 +65,8 @@ runs: CI_UV: ${{ inputs.uv-version }} run: | set -euo pipefail - version="$(tr -d '[:space:]' < .python-version)" - test -n "$version" + version="$(python -c 'import pathlib, tomllib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))["project"]["requires-python"].removeprefix("=="))')" + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo 'Expected an exact requires-python pin in pyproject.toml' >&2; exit 1; } cache_root="${CI_TEMP//\\//}" identity="$CI_RUNNER-$CI_OS-$CI_ARCH-py-$version-uv-$CI_UV" { diff --git a/.github/workflows/post-merge.yml b/.github/workflows/post-merge.yml index 3d99fe81d3..ff7f0130c9 100644 --- a/.github/workflows/post-merge.yml +++ b/.github/workflows/post-merge.yml @@ -95,6 +95,7 @@ jobs: fetch-depth: 0 - name: Set up CI environment + id: environment uses: ./.github/actions/ci-environment with: uv-version: ${{ env.CI_UV_VERSION }} @@ -107,6 +108,7 @@ jobs: GH_TOKEN: ${{ github.token }} RELEASE_REPOSITORY: ${{ github.repository }} RELEASE_COMMIT: ${{ inputs.release_commit || github.sha }} + CI_PYTHON: ${{ steps.environment.outputs.python-version }} run: | - uv run --no-project --python "$(cat .python-version)" python -m scripts.publish_release \ + uv run --no-project --python "$CI_PYTHON" python -m scripts.publish_release \ --repository "$RELEASE_REPOSITORY" --commit "$RELEASE_COMMIT" diff --git a/.python-version b/.python-version deleted file mode 100644 index 12566ed7f9..0000000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.14.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a3c77416e..01dde12470 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,18 +11,17 @@ Thanks for helping improve Free Claude Code. Keep changes focused, test the beha ## Pull Request Titles -Changes to `assets/`, `scripts/`, `src/`, `.python-version`, `pyproject.toml`, or `uv.lock` require a title starting with `patch: `, `minor: `, or `major: ` followed by a description. Use exactly one space after the colon. +Changes to `assets/`, `scripts/`, `src/`, `pyproject.toml`, or `uv.lock` require a title starting with `patch: `, `minor: `, or `major: ` followed by a description. Use exactly one space after the colon. PRs without changes to those paths must not use a release prefix. Do not edit version numbers. Publishing calculates the version after merge, creates its Git tag, and publishes to PyPI and GitHub Releases. ## Development Setup -Install [uv](https://docs.astral.sh/uv/) and Python 3.14, then run directly from the checkout: +Install [uv](https://docs.astral.sh/uv/), then run directly from the checkout. uv automatically installs the Python version required by the project. ```bash git clone https://github.com/Alishahryar1/free-claude-code.git cd free-claude-code -uv python install 3.14.0 uv run fcc-server ``` diff --git a/e2e/test_progressive_startup.py b/e2e/test_progressive_startup.py index c1233e034a..5a3a62005c 100644 --- a/e2e/test_progressive_startup.py +++ b/e2e/test_progressive_startup.py @@ -73,7 +73,16 @@ def status(route): route.fulfill(json=data) page.route("**/admin/api/status", status) - page.goto(f"{admin_base_url}/admin/integrations") + with ( + page.expect_response( + "**/admin/api/integrations/claude-vscode" + ) as claude_status, + page.expect_response("**/admin/api/integrations/codex") as codex_status, + ): + page.goto(f"{admin_base_url}/admin/integrations") + claude_status.value.finished() + codex_status.value.finished() + page.wait_for_function("state.startup?.startup?.catalog_file === 'starting'") connect = page.locator("#openCodexIntegration") expect(connect).to_be_disabled() expect(connect).to_have_attribute("aria-busy", "true") diff --git a/pyproject.toml b/pyproject.toml index 955c2e89a8..16f7131e7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Local proxy connecting coding agents to OpenAI-compatible AI prov readme = "README.md" license = "AGPL-3.0-only" license-files = ["LICENSE"] -requires-python = ">=3.14.0" +requires-python = "==3.14.7" dependencies = [ "anyio>=4.15.1", "fastapi[standard]>=0.141.1", diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 1a6a569a3e..f141b5ab1a 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -13,7 +13,7 @@ $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" # Windows on ARM emulates x64, whose Python package ecosystem has broader wheel support. -$PythonRequest = "cpython-3.14.0-windows-x86_64-none" +$PythonRequest = "cpython-3.14.7-windows-x86_64-none" $MinUvVersion = "0.12.13" $ClaudeInstallUrl = "https://claude.ai/install.ps1" $CodexInstallUrl = "https://chatgpt.com/codex/install.ps1" diff --git a/scripts/install.sh b/scripts/install.sh index 06ab26455d..61418c2876 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -PYTHON_VERSION="3.14.0" +PYTHON_VERSION="3.14.7" MIN_UV_VERSION="0.12.13" CLAUDE_INSTALL_URL="https://claude.ai/install.sh" CODEX_INSTALL_URL="https://chatgpt.com/codex/install.sh" diff --git a/tests/contracts/test_uv_policy.py b/tests/contracts/test_uv_policy.py index 086dc888df..5960c24c5d 100644 --- a/tests/contracts/test_uv_policy.py +++ b/tests/contracts/test_uv_policy.py @@ -1,3 +1,8 @@ +import os +import re +import shutil +import subprocess +import sys import tomllib from pathlib import Path @@ -18,6 +23,75 @@ } +@pytest.mark.parametrize( + "requirement, valid", + [ + ('"==3.14.7"', True), + ("'==3.14.7'", True), + ('"==3.14.7" # Python runtime', True), + ('">=3.14.7"', False), + ], +) +def test_ci_python_identity_parses_toml( + tmp_path: Path, requirement: str, valid: bool +) -> None: + setup = yaml.safe_load(CI_SETUP.read_text(encoding="utf-8")) + step = next(step for step in setup["runs"]["steps"] if step.get("id") == "identity") + (tmp_path / "pyproject.toml").write_text( + f"[project]\nrequires-python = {requirement}\n", encoding="utf-8" + ) + bash = shutil.which("bash") + if sys.platform == "win32": + git = shutil.which("git") + assert git is not None + bash = str(Path(git).resolve().parent.parent / "bin" / "bash.exe") + assert bash is not None + env = { + **os.environ, + "PATH": str(Path(sys.executable).parent) + os.pathsep + os.environ["PATH"], + "CI_TEMP": tmp_path.as_posix(), + "CI_RUNNER": "test-runner", + "CI_OS": "test-os", + "CI_ARCH": "X64", + "CI_UV": UV_MINIMUM, + "GITHUB_ENV": "github-env", + "GITHUB_OUTPUT": "github-output", + } + result = subprocess.run( + [bash, "-c", step["run"]], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + timeout=30, + ) + if valid: + assert result.returncode == 0, result.stderr + output = (tmp_path / "github-output").read_text(encoding="utf-8") + assert "python-version=3.14.7\n" in output + assert f"py-3.14.7-uv-{UV_MINIMUM}" in output + else: + assert result.returncode != 0 + assert not (tmp_path / "github-output").exists() + + +def test_installer_python_requests_match_package_requirement() -> None: + project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8")) + requirement = project["project"]["requires-python"] + match = re.fullmatch(r"==([0-9]+\.[0-9]+\.[0-9]+)", requirement) + assert match is not None + version = match[1] + shell = Path("scripts/install.sh").read_text(encoding="utf-8") + powershell = Path("scripts/install.ps1").read_text(encoding="utf-8") + shell_pin = re.search(r'^PYTHON_VERSION="([^"]+)"', shell, re.MULTILINE) + windows_pin = re.search(r'^\$PythonRequest = "([^"]+)"', powershell, re.MULTILINE) + assert shell_pin is not None and windows_pin is not None + assert shell_pin[1] == version + assert windows_pin[1] == f"cpython-{version}-windows-x86_64-none" + lock = tomllib.loads(Path("uv.lock").read_text(encoding="utf-8")) + assert lock["requires-python"] == requirement + + def _assert_required_test_matrix(workflow: dict, suite: str) -> None: job = workflow["jobs"][suite] assert job["name"] == f"{suite} (${{{{ matrix.os }}}})" diff --git a/tests/installers/test_installers.py b/tests/installers/test_installers.py index 6750230dfd..9f0938df2b 100644 --- a/tests/installers/test_installers.py +++ b/tests/installers/test_installers.py @@ -838,7 +838,7 @@ def test_install_sh_fresh_install_is_verified(posix_harness: PosixHarness) -> No assert any( call.startswith( "uv:tool install --force --refresh-package free-claude-code " - "--python 3.14.0 free-claude-code" + "--python 3.14.7 free-claude-code" ) for call in calls ) @@ -3186,7 +3186,7 @@ def test_install_ps1_fresh_install_is_verified( assert any( call.startswith( "uv:tool install --force --refresh-package free-claude-code " - "--python cpython-3.14.0-windows-x86_64-none " + "--python cpython-3.14.7-windows-x86_64-none " "free-claude-code" ) for call in calls @@ -4237,7 +4237,7 @@ def test_installers_use_native_clients_and_single_python_selection() -> None: def test_install_ps1_uses_x64_python_for_windows_arm_compatibility() -> None: powershell = (_repo_root() / "scripts" / "install.ps1").read_text(encoding="utf-8") - assert '$PythonRequest = "cpython-3.14.0-windows-x86_64-none"' in powershell + assert '$PythonRequest = "cpython-3.14.7-windows-x86_64-none"' in powershell @pytest.mark.parametrize("powershell", _powershells()) diff --git a/uv.lock b/uv.lock index ef50412694..56789f0e54 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.14.0" +requires-python = "==3.14.7" [[package]] name = "accelerate" @@ -123,7 +123,7 @@ version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.15'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ @@ -804,26 +804,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" }, - { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" }, - { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" }, - { url = "https://files.pythonhosted.org/packages/65/53/4e13642efc4d7ad6554ecb2242a5be42666b2e1a067323e88dfc0124a04b/greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76", size = 681436, upload-time = "2026-08-10T14:30:14.839Z" }, - { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" }, - { url = "https://files.pythonhosted.org/packages/cd/32/188447c9a468d6977d2989397226b0c6b65ab6f4cf943f931643328512fc/greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474", size = 487404, upload-time = "2026-08-10T14:30:08.903Z" }, - { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" }, - { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" }, - { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" }, - { url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" }, - { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" }, - { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f4/e450a68a152f819491d8c7df6a8254e761d87e6a78759268961f8c5bd4dd/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1", size = 686022, upload-time = "2026-08-10T14:30:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/17e63d6bf3b9c9b9dbea981b7f643a71f79603bdfb4f1c3a9cf353e22aed/greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f", size = 516951, upload-time = "2026-08-10T14:30:10.907Z" }, - { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" }, - { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" }, - { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, ] [[package]] @@ -845,16 +825,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/c4/3e1c3d6155c16b8737cc31d5b477d6cf1fc7cdd10d58320cf0ec9b446f42/grpcio-1.84.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5559b492007dc09b4de9b95dab05f0b5e53547aad230cf07e46c7dd017a3be5", size = 8123299, upload-time = "2026-09-14T06:58:54.332Z" }, { url = "https://files.pythonhosted.org/packages/56/fe/f4864de5b815e5ba18858771f99381a398fac14117f89ef5291ed43d3c4e/grpcio-1.84.0-cp314-cp314-win32.whl", hash = "sha256:2c024da73b296f040b8360e60bd73a659b230093684a438da0e1260f34cc724e", size = 4562560, upload-time = "2026-09-14T06:58:56.894Z" }, { url = "https://files.pythonhosted.org/packages/44/03/640811d4d8c84f5e603995c5a9bab725223aa472cad9ca4286c3bbf1c3e3/grpcio-1.84.0-cp314-cp314-win_amd64.whl", hash = "sha256:800b7e00d92553313c0463c200087930aa78678ec1d528193aeb50906f55989b", size = 5394092, upload-time = "2026-09-14T06:58:59.61Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1a/9e3d2c9f005f680f03308fa894b1db91d4ab3f0fe65ff630c69561e91e95/grpcio-1.84.0-cp315-cp315-linux_armv7l.whl", hash = "sha256:47ecf0d9b81d981f07b61bd89eced9d2582f5eaacc3aaa36ad27f81aef70a27f", size = 6428252, upload-time = "2026-09-14T06:59:02.597Z" }, - { url = "https://files.pythonhosted.org/packages/77/34/0bc9f52ebf091311651eeab3a452fb557985604a3088cb5406f4d6df85d3/grpcio-1.84.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:61386101ecaa096b694d0dd278caf99a56aeec78440cc17e918eef0b50f2d567", size = 12359488, upload-time = "2026-09-14T06:59:05.646Z" }, - { url = "https://files.pythonhosted.org/packages/93/0e/c31052712f241cb6ecae9c226fabd519b7f8c64a7a40bac27e9ca0405b78/grpcio-1.84.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6d178ba6dc8e82976c184b65fddde172d054c17237993a3e083efe4f134d55b", size = 7019339, upload-time = "2026-09-14T06:59:08.76Z" }, - { url = "https://files.pythonhosted.org/packages/55/b9/b9b33ea4f1eb4cad28833cade604febf357385b5ebb0c9c7562d020e167a/grpcio-1.84.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:15bb76489e337fc492685c9758e2fd4d4ab516b901ad830dc5a91987decf00be", size = 7107974, upload-time = "2026-09-14T06:59:11.568Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9e/799d4c45db91bbdcd8c54b3982932dbcf3d059f7ce67dca3e8540faa1ece/grpcio-1.84.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82da34ae4f639c73ac46e521e00c0a49bf86f717b9fb1f405f133e98731e38dc", size = 7200036, upload-time = "2026-09-14T06:59:14.401Z" }, - { url = "https://files.pythonhosted.org/packages/45/dc/dcfdd13ada41aff9098f0c2c6f260eb7debbc88b84b7e5fcbd085165427d/grpcio-1.84.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9b73836ba0e16fcbb57c31cf6cbc2907c8d8c790b83679df454b74bd15e0be04", size = 7742281, upload-time = "2026-09-14T06:59:17.348Z" }, - { url = "https://files.pythonhosted.org/packages/55/31/75eab2ec77b80804bc5e21cec99b57598e726fca6484cd3e8920a97639d5/grpcio-1.84.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:42959bd50dd660ffc3f2a9bec15a6da4f9aaa0dda555d59ff2d2e80b908456a8", size = 8113629, upload-time = "2026-09-14T06:59:20.584Z" }, - { url = "https://files.pythonhosted.org/packages/34/f0/fdcf6bdc1df9ca11679a1187bef8e6b81df31a2baae69497e17344f05ea3/grpcio-1.84.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:659728f20fc7a0933ed7b1945435e31014b97ab8a5a7edcbaa70da4794aeb191", size = 8152972, upload-time = "2026-09-14T06:59:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cf/6720e720bfa80fcb1ace873f66724eb3c8b03bba2fa078a30c12cab3212e/grpcio-1.84.0-cp315-cp315-win32.whl", hash = "sha256:edb6f87fc60ff438557291501b3e16c7a77c3b01a52d782cf276dccc7c5dd89c", size = 4561981, upload-time = "2026-09-14T06:59:27.275Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b9/69d8a709df225bc2e06e028e9465166b174c24b3da07cc72d9a5ddc63194/grpcio-1.84.0-cp315-cp315-win_amd64.whl", hash = "sha256:4119efa6519871719ad81f33bc95ab87857dcb1c5801f30a6e592f2c41164169", size = 5394757, upload-time = "2026-09-14T06:59:30.118Z" }, ] [[package]] @@ -1598,19 +1568,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, - { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, - { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, - { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, - { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, - { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, - { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, - { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, - { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, - { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, ] [[package]] @@ -1887,8 +1844,6 @@ sdist = { url = "https://files.pythonhosted.org/packages/b4/b1/729f7458a63758bd2 wheels = [ { url = "https://files.pythonhosted.org/packages/bd/8a/cfa4f56939d554dbb342ec6e5226a441e2f552bc2002a0ddf7705bb11bef/pyobjc_core-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2b8fc0531c27277325e113ac00b8a72a82e6145f0a88175b9425d8de814ff69a", size = 6429289, upload-time = "2026-06-19T16:05:02.191Z" }, { url = "https://files.pythonhosted.org/packages/42/74/446c89bc18103aaa4a00d1fb85ff8acace9a0dc3f362d9678ebf7571e275/pyobjc_core-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9bef500f979e22d54f9da3aaebf6a48f873234b324858bd69256055a318955c7", size = 6690181, upload-time = "2026-06-19T16:05:06.201Z" }, - { url = "https://files.pythonhosted.org/packages/99/c7/0121ee4c616af07ad2de8cd1a286f6978dc9a227eb58b7c2e875cb68a1df/pyobjc_core-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:047c226eeb58a2993ace5e8904e71cc9426ee20d064c617f8fbf32717d37093e", size = 6487078, upload-time = "2026-06-19T16:05:10.093Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a8/cb9fcc150f97d0bf22a2028f88b24cc35949beb1bcc7b8bc5c17d4401677/pyobjc_core-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1188613805336270279570467e4455b74cb6c0f60913ac74c917ee1c37cfaecb", size = 6733064, upload-time = "2026-06-19T16:05:14.313Z" }, ] [[package]] @@ -1902,8 +1857,6 @@ sdist = { url = "https://files.pythonhosted.org/packages/51/34/fbe38a204643aa4e1 wheels = [ { url = "https://files.pythonhosted.org/packages/ec/c8/b90baa8f3592eded79b4be98fb59d2b8dc16b62361e34292bd95806ebd9f/pyobjc_framework_cocoa-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b386c324d64ae565c1f6b7dfb77be68f640a1c7c23caa6966ab661131f519561", size = 388357, upload-time = "2026-06-19T16:07:43.364Z" }, { url = "https://files.pythonhosted.org/packages/98/d8/64a94651b9294702d55e748d94de30e25bc59d0784526be7643f4467eccd/pyobjc_framework_cocoa-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a6c584e2af0813cb2f6103b184e632665a26f58c1bd5b08ffd6e95a19c617f7b", size = 392404, upload-time = "2026-06-19T16:07:44.955Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cc/26e8a7bf1f5e8caa38b7f80d486296f9fd3c97e71ad7e5444ef22e802758/pyobjc_framework_cocoa-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:b6023657b8d6cc049a21bd6b4752425f2f53c42f9f0b02d64c7608cc484bf103", size = 388589, upload-time = "2026-06-19T16:07:46.276Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/eedf743a303ea742b8e082afe3613fb4d6618bc1a48cf2568b004ce906f7/pyobjc_framework_cocoa-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:c685ccd8e266a07cf912a2c5a13b1f2eff2a868a1aff163b4801b4687bd425e1", size = 392691, upload-time = "2026-06-19T16:07:47.477Z" }, ] [[package]] @@ -1918,8 +1871,6 @@ sdist = { url = "https://files.pythonhosted.org/packages/3b/f6/2a8b84dbf1fe7c04d wheels = [ { url = "https://files.pythonhosted.org/packages/8b/5d/85ffd9d433989205d572a50d625c63b29c05e0c5235a725f15ae1023672c/pyobjc_framework_quartz-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ceb56939c337b36d9d81185ade31f77dc52c85cf79bb16e53e9b32f54b6bb3f5", size = 219769, upload-time = "2026-06-19T16:16:08.814Z" }, { url = "https://files.pythonhosted.org/packages/e2/d6/b917e4b63d72ea84a27121076f3033f23f6497c0e6ce8d304766c899897f/pyobjc_framework_quartz-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8105c98b798f2bf81c05c54bddeeadbf62f0b5dfec13bd6e719dd2cdf7e1cddf", size = 224717, upload-time = "2026-06-19T16:16:10.215Z" }, - { url = "https://files.pythonhosted.org/packages/04/e2/f3c1ed3228f7430ef5ade23db6f1fcbae99290f177ce5653348fd9e05f4d/pyobjc_framework_quartz-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:bbc214f1a216b5d3651bc832d0ac4589f029f3f37cd6cbb370aac12a7c77942c", size = 219825, upload-time = "2026-06-19T16:16:11.433Z" }, - { url = "https://files.pythonhosted.org/packages/66/2a/2c99a5ad2fe0a11600ea123b8e9a08ff138fcb2ad1e13e376f4bd4aa1d96/pyobjc_framework_quartz-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:ca61624a0b0e6286d8a0f97f47eb9011e4e81e9a339db436d48af527e7065bb1", size = 224770, upload-time = "2026-06-19T16:16:13.035Z" }, ] [[package]] @@ -2281,35 +2232,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, - { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, - { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, - { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, - { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, - { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, - { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, - { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, - { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, - { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, - { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, - { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, - { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, - { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, - { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, - { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, - { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, - { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, - { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, ] [[package]] @@ -2613,20 +2535,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/ce/af8964c38bc8226dd8950305b7a255fa33345d5572f78af7275a313d28e0/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2fc834fbe3f6a0736905c36ab709537e6840dbd63b982dc9e0216ae7d305ba1a", size = 1250932, upload-time = "2026-08-17T19:49:19.28Z" }, { url = "https://files.pythonhosted.org/packages/1d/4b/323631116fc986d9cc5bbeb2b8223c7c85e61a8bb94ea5ab4951023b149b/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ca4db6ff5c5bf600f9b7761a0070ed44dfe5797a76bd432fb978bc480ef40c58", size = 1315190, upload-time = "2026-08-17T19:49:20.467Z" }, { url = "https://files.pythonhosted.org/packages/18/8b/ba48a73729c9270989b36f37ab2ed5525e52690d715097c9fa791aaa5d05/tiktoken-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7aab286a020660a039097912a088236b985d18a3090d73f136c4413d29d37ca0", size = 987717, upload-time = "2026-08-17T19:49:21.704Z" }, - { url = "https://files.pythonhosted.org/packages/1d/10/b73b7e319179e0f60b32475f783b044f9cece872c53b6662664e9084b0d0/tiktoken-0.14.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:14b47e3674f2624803a8acc8fb367b7e24fc53055f9df3296482fe9a3a34a232", size = 1096280, upload-time = "2026-08-17T19:49:22.779Z" }, - { url = "https://files.pythonhosted.org/packages/c2/6b/09999a9bf1d559670d1680e8f8e419ac0e2c5f6aac82e9bfdf70f260b30a/tiktoken-0.14.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:19d643d701fdaa70e5b9c7f8f96abcaffe77ca5e482a3a1a7dde46feb4284695", size = 1040433, upload-time = "2026-08-17T19:49:23.998Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7b/8537be0836f3df99b2a636b44399bfa43cd757f2b8b4097dacb794cf24a7/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:e4ddf863b59347deaa92302dcd90e5eb003cdc9be06ec2b692c38d1bdd9efd49", size = 1186989, upload-time = "2026-08-17T19:49:25.021Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9d/f9c56d7a943a4468abf9ef37661bb9b8e0cd3aa8aa87368c7146cc3f3222/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:60c47ca69ddda0dea8256fffd12e1b86f4b59734a20e4a70c61f63cc5f021df4", size = 1204615, upload-time = "2026-08-17T19:49:26.37Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d2/98a38579db25c4a8a84e31dd95d9072ec5f21f7e70de591da0412e29b25b/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:728303a072163130c5b477b1f20d6211895569c1d5302c24ffc93a3009160871", size = 1251828, upload-time = "2026-08-17T19:49:27.423Z" }, - { url = "https://files.pythonhosted.org/packages/0c/83/467be424746c039c5493c0f4102feab16b9b48eb6f5c089b2a2438e3cde2/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3c5349c9f916283bba32bec8af69b763e4faa304dc004d0eaaea66a3cf004c1f", size = 1316260, upload-time = "2026-08-17T19:49:29.101Z" }, - { url = "https://files.pythonhosted.org/packages/02/ee/ddf46ca78e371f5890e96b6e7d089a85b3536432be219851eb0481786ca8/tiktoken-0.14.0-cp315-cp315-win_amd64.whl", hash = "sha256:1b6e4adcfd285c44502aed51df98aaaca4f0fea028165dbf8a9e857b9f98d8ea", size = 988230, upload-time = "2026-08-17T19:49:30.246Z" }, - { url = "https://files.pythonhosted.org/packages/2a/00/5162e90c851a28da18ed382d34898b79a8022548e5619a64e14c03ce7c3d/tiktoken-0.14.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:11d8211b290855d2721334ff17dd9b3a17bfb26872be01f25d73612ef7ece890", size = 1096186, upload-time = "2026-08-17T19:49:31.656Z" }, - { url = "https://files.pythonhosted.org/packages/65/97/a5a7bfccf25b1bb65e82bae8edff11ac3c9c041c374b7b4a823d60c38133/tiktoken-0.14.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d0781223705199b289faa59601bb9c2441712d4c600dd13c43d8fd6a33d22cd5", size = 1039947, upload-time = "2026-08-17T19:49:32.848Z" }, - { url = "https://files.pythonhosted.org/packages/fb/ba/ef427fc638f1439181c5e12dd26b70e881861f89c007aa7e5b36300f8342/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:2ea70afba6b9eddbf22c165142e5f0a2ad7aa36a452873c48b57bb2aeb8492ae", size = 1186997, upload-time = "2026-08-17T19:49:34.121Z" }, - { url = "https://files.pythonhosted.org/packages/3e/88/2f3f85a968cdc514152129af0a060ebcccb067005a2f29b0d5ef3c838514/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:78571efc311c30b73f31eb949a921d6dac39a5d9dc42d1cfa8f8db157b3447b1", size = 1205211, upload-time = "2026-08-17T19:49:35.284Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f6/80760e98a08e6649d2d68afb6035af713121dfb615acce8c4f73810ec438/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86f66c85e796f5d05d5c4a60ec1d40cbfebc47a32464053528c797163fa9ab89", size = 1251479, upload-time = "2026-08-17T19:49:36.419Z" }, - { url = "https://files.pythonhosted.org/packages/c5/84/50966fb6918a0fb9b32721277e5342bf729a2d74350074d662fbedf9772e/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:149d97453c4c98c04b081d64a85e635921269b532710d6faf81e9e82b790e7d3", size = 1316673, upload-time = "2026-08-17T19:49:37.756Z" }, - { url = "https://files.pythonhosted.org/packages/35/5e/9b01afd037bfa22a0033963fa091e0f75b6fb15cd85bffb42ff86e697323/tiktoken-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9", size = 987929, upload-time = "2026-08-17T19:49:38.947Z" }, ] [[package]] @@ -2670,7 +2578,7 @@ name = "torch" version = "2.14.0+cu130" source = { registry = "https://download.pytorch.org/whl/cu130" } dependencies = [ - { name = "cuda-bindings", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, { name = "filelock" }, { name = "fsspec" }, @@ -2682,7 +2590,7 @@ dependencies = [ { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, { name = "setuptools" }, { name = "sympy" }, - { name = "triton", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "triton", marker = "sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ @@ -2692,12 +2600,6 @@ wheels = [ { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.14.0%2Bcu130-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6c6f734dc5bd126316a9e6320cff848b0e21afc1702118dd504fa45512c0d2d5", upload-time = "2026-09-02T18:55:47Z" }, { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.14.0%2Bcu130-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9a5dfdca3375869ae89803ae79d44531b706af9352a77066b8bdf134de0e09d4", upload-time = "2026-09-02T18:56:03Z" }, { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.14.0%2Bcu130-cp314-cp314t-win_amd64.whl", hash = "sha256:7db94ac0c41f1e0c10efd532ab43150c681edfc823cd5332c307644573403840", upload-time = "2026-09-02T18:56:39Z" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.14.0%2Bcu130-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:b82c5857f2c397a285027b65093dc8c89c7b45a69535786a69dae462bf67fd07", upload-time = "2026-09-02T18:57:13Z" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.14.0%2Bcu130-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:9e91cca5c03701158d783fb91fc1c5651469f466a3a3bcb4df5bcbff36cfd371", upload-time = "2026-09-02T18:57:26Z" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.14.0%2Bcu130-cp315-cp315-win_amd64.whl", hash = "sha256:fca94b3181939ca5bdfc1f7d108a79888dec750f3683f922bda40dc4e78d6eaa", upload-time = "2026-09-02T18:57:59Z" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.14.0%2Bcu130-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:16171adcc338c0827fbc28e448b96071638f4733743ee144b9692ec43477d821", upload-time = "2026-09-02T18:58:34Z" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.14.0%2Bcu130-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:9e68cd6d619325acf1d94fc0c0a819e7a19dd95d226744be20acf7b7aff3ac8a", upload-time = "2026-09-02T18:58:46Z" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.14.0%2Bcu130-cp315-cp315t-win_amd64.whl", hash = "sha256:c9d9ebc6c552c9e1b1779838b3c236f22e34fac2bcd441c1db8c939dd15f6fff", upload-time = "2026-09-02T18:59:19Z" }, ] [[package]]