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..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 }} @@ -44,6 +47,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 @@ -55,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/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/post-merge.yml b/.github/workflows/post-merge.yml index 55ea434354..ff7f0130c9 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,23 +86,16 @@ 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 + id: environment uses: ./.github/actions/ci-environment with: uv-version: ${{ env.CI_UV_VERSION }} @@ -105,38 +103,12 @@ 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 }} + CI_PYTHON: ${{ steps.environment.outputs.python-version }} 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 "$CI_PYTHON" 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..0bc27f3808 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 @@ -173,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/.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/.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 1037a71cf0..01dde12470 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,14 +9,19 @@ 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/`, `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 ``` @@ -54,7 +59,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/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/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/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 4e62a6cdb7..16f7131e7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,19 @@ [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" license-files = ["LICENSE"] -requires-python = ">=3.14.0" +requires-python = "==3.14.7" 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", ] @@ -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" } @@ -83,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/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/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/scripts/publish_release.py b/scripts/publish_release.py new file mode 100644 index 0000000000..e1daf46c0a --- /dev/null +++ b/scripts/publish_release.py @@ -0,0 +1,221 @@ +"""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 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( + [ + "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: + 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: + 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, release["id"], 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, release_id: int, version: str, source: Path, dist: Path + ) -> None: + names = set(distribution_names(version)) + 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 = ( + {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.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: + 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..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 @@ -9,6 +14,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", @@ -17,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/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/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/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/scripts/test_publish_release.py b/tests/scripts/test_publish_release.py new file mode 100644 index 0000000000..51c56a9f32 --- /dev/null +++ b/tests/scripts/test_publish_release.py @@ -0,0 +1,310 @@ +"""Release ordering and interrupted publication use real Git history.""" + +import hashlib +import json +from itertools import pairwise +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): + id: int + 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": { + "id": 1, + "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"]: + 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) + 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("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 + 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/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 489a472d91..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 = [ @@ -599,7 +599,6 @@ wheels = [ [[package]] name = "free-claude-code" -version = "6.2.66" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -658,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" }, @@ -670,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" }, @@ -682,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]] @@ -751,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" }, @@ -759,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]] @@ -805,47 +804,27 @@ 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]] 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" } +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/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" }, + { 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" }, ] [[package]] @@ -1546,7 +1525,7 @@ wheels = [ [[package]] name = "openai" -version = "3.14.0" +version = "3.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1556,18 +1535,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]] @@ -1589,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]] @@ -1878,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]] @@ -1893,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]] @@ -1909,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]] @@ -2272,60 +2232,31 @@ 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]] 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]] @@ -2604,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]] @@ -2661,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" }, @@ -2673,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 = [ @@ -2683,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]] @@ -2745,27 +2656,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]] @@ -2815,15 +2726,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]