Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .castiron.stats.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
schema_version: 1
generation_id: b67f6675-ad30-42fd-90ec-33d09485b9a0
openapi_spec_hash: 32e9f9c8096eae027754def9afb89dc0
openapi_transformed_spec_hash: a5861d7f7b67f6845c9499fdec7324a5
generation_id: dd4c6a7b-158a-4b11-9b4a-128435aaa1f0
openapi_spec_hash: 4af7018ff56446e0398fc7946a92680f
openapi_transformed_spec_hash: b2689771e08c3fdc6ae8f0433dc2c784
config_hash: 5c00fe18ea913ed0186f9894adc02a70
codegen_sha: 9177ce998eec68cf1d241f1efd84f8786eba92cd
codegen_hash: 9271ebabb8723d763a7a728f3a455fb1ee4ea678e20885f891b50222f68d4def
public_codegen_sha: 2951af18b699be3114ff5e6a3c7f7a669e7d5a76
codegen_sha: 9bc71a62e18aa941ba073325f46e29ff05e1e90c
codegen_hash: 803a072c019069cce4b17ef21da0980c4d6086b7ba935ff45b9d8b9f00d7acdd
public_codegen_sha: 734497bdc1d37e9709c026b67eaec668fe37f753
27 changes: 27 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,41 @@ updates:
default-days: 8
- package-ecosystem: uv
directory: /
versioning-strategy: increase-if-necessary
schedule:
interval: weekly
day: monday
time: "09:00"
timezone: Etc/UTC
# Version updates wait; Dependabot security updates are exempt.
cooldown:
default-days: 8
open-pull-requests-limit: 3
groups:
python-maintenance:
applies-to: version-updates
patterns:
- "*"
update-types:
- minor
- patch
python-security:
applies-to: security-updates
patterns:
- "*"

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
time: "09:30"
timezone: Etc/UTC
cooldown:
default-days: 8
open-pull-requests-limit: 5
groups:
# CodeQL init/analyze share versioned state and must change together.
codeql:
patterns:
- "github/codeql-action"
59 changes: 58 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,59 @@ on:
permissions:
contents: read

env:
UV_NO_BUILD: '1'
UV_NO_BINARY_PACKAGE: 'openai'
UNTRUSTED_BUILD_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id && '1' || '0' }}
TRUSTED_BUILD_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }}

jobs:
dependency-locks:
timeout-minutes: 10
name: dependency lock freshness
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Verify dependency source provenance before installing tools
run: |
python -I -c 'import ast, os, pathlib, re, subprocess, tomllib; config = tomllib.loads(pathlib.Path("pyproject.toml").read_text()); fork = os.environ.get("UNTRUSTED_BUILD_FORK", "0") == "1"; base = os.environ.get("TRUSTED_BUILD_BASE_SHA", ""); assert not fork or re.fullmatch(r"[0-9a-f]{40}", base), "Untrusted fork build base commit"; assert not fork or subprocess.check_output(["git", "remote", "get-url", "origin"], text=True).strip() in {"https://github.com/openai/openai-python", "https://github.com/openai/openai-python.git"}, "Untrusted fork build base origin"; subprocess.run(["git", "fetch", "--no-tags", "--depth=1", "origin", base], check=True) if fork else None; trusted = tomllib.loads(subprocess.check_output(["git", "show", base + ":pyproject.toml"], text=True)) if fork else config; hatch = lambda document: document.get("tool", {}).get("hatch", {}); executable = lambda document: (hatch(document).get("build", {}).get("hooks", {}), tuple(sorted((name, target.get("hooks", {})) for name, target in hatch(document).get("build", {}).get("targets", {}).items() if target.get("hooks", {}))), hatch(document).get("metadata", {}).get("hooks", {})); assert not fork or executable(config) == executable(trusted), "Untrusted fork Hatch executable hook configuration"; sections = executable(config); paths = [definition.get("path", "hatch_build.py") for mapping in [sections[0], *[item[1] for item in sections[1]], sections[2]] for plugin, definition in mapping.items() if plugin == "custom" or "path" in definition] if fork else []; assert not fork or len(paths) <= 32 and all(isinstance(location, str) and bool(location) and len(location) <= 512 and not (candidate := pathlib.Path(location)).is_absolute() and bool(candidate.parts) and ".." not in candidate.parts and candidate.is_file() and all(not pathlib.Path(*candidate.parts[:index]).is_symlink() for index in range(1, len(candidate.parts) + 1)) and candidate.read_bytes() == subprocess.check_output(["git", "show", base + ":" + candidate.as_posix()]) for location in paths), "Untrusted fork Hatch executable hook source"; project = config["project"]; lock = tomllib.loads(pathlib.Path("uv.lock").read_text()); trusted_lock = tomllib.loads(subprocess.check_output(["git", "show", base + ":uv.lock"], text=True)) if fork else lock; roots = [package for package in lock["package"] if package.get("name") == project["name"] and package.get("version") == project["version"] and package.get("source") == {"editable": "."}]; build = config.get("build-system", {}); uv = config.get("tool", {}).get("uv", {}); requires = build.get("requires", []); constraints = uv.get("build-constraint-dependencies", []); reviewed = config.get("dependency-groups", {}).get("build", []); canonical = lambda value: re.sub(r"[-_.]+", "-", value).lower(); package_identity = lambda package: (canonical(package["name"]), package["version"], package.get("source", {}).get("registry", ""), tuple(sorted((artifact["url"], artifact["hash"]) for artifact in (([package["sdist"]] if "sdist" in package else []) + package.get("wheels", []))))); normalize = lambda value: re.sub(r"\s+", "", value); identity = lambda value: (canonical((item := re.fullmatch(r"([A-Za-z0-9][A-Za-z0-9_.-]*)\s*==\s*([0-9]+(?:\.[0-9]+)*(?:\.post[0-9]+)?)(?:\s*;\s*(.+))?", value)).group(1)), item.group(2), ast.dump(ast.parse(item.group(3), mode="eval")) if item.group(3) is not None else ""); assert len(roots) == 1 and canonical(project["name"]) == "openai" and sum(canonical(package.get("name", "")) == "openai" for package in lock["package"]) == 1 and all(package is roots[0] or package.get("source") == {"registry": "https://pypi.org/simple"} and isinstance(package.get("wheels"), list) and bool(package.get("wheels")) and (artifacts := ([package["sdist"]] if "sdist" in package else []) + package.get("wheels", [])) and all(isinstance(artifact, dict) and re.fullmatch(r"https://files\.pythonhosted\.org/packages/[^\s?#]+", artifact.get("url", "")) and re.fullmatch(r"sha256:[0-9a-f]{64}", artifact.get("hash", "")) for artifact in artifacts) for package in lock["package"]) and (not fork or sorted(package_identity(package) for package in lock["package"] if package is not roots[0]) == sorted(package_identity(package) for package in trusted_lock["package"] if package.get("source") != {"editable": "."})) and build.get("build-backend") == "hatchling.build" and "backend-path" not in build and isinstance(requires, list) and bool(requires) and isinstance(constraints, list) and bool(constraints) and isinstance(reviewed, list) and bool(reviewed) and not any(uv.get(key) for key in ("sources", "index", "indexes", "index-url", "extra-index-url", "default-index", "find-links", "index_url", "extra_index_url", "default_index", "find_links", "no-binary", "no-binary-package", "no_binary", "no_binary_package")) and not any(pathlib.Path(path).exists() for path in ("uv.toml", ".uv.toml")) and all(isinstance(requirement, str) and (match := re.fullmatch(r"([A-Za-z0-9][A-Za-z0-9_.-]*)\s*==\s*([0-9]+(?:\.[0-9]+)*(?:\.post[0-9]+)?)(?:\s*;\s*(.+))?", requirement)) is not None and (match.group(3) is None or isinstance(marker := ast.parse(match.group(3), mode="eval").body, ast.Compare) and isinstance(marker.left, ast.Name) and len(marker.ops) == 1 and type(marker.ops[0]) in {ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn} and len(marker.comparators) == 1 and isinstance(marker.comparators[0], ast.Constant) and isinstance(marker.comparators[0].value, str)) and any(canonical(package.get("name", "")) == canonical(match.group(1)) and package.get("version") == match.group(2) and package.get("source") == {"registry": "https://pypi.org/simple"} for package in lock["package"]) for requirement in requires + constraints + reviewed) and {normalize(requirement) for requirement in constraints} == {normalize(requirement) for requirement in reviewed} and {normalize(requirement) for requirement in requires}.issubset({normalize(requirement) for requirement in constraints}) and any(canonical(requirement.split("==", 1)[0].strip()) == "hatchling" for requirement in requires) and build.get("build-backend") == trusted.get("build-system", {}).get("build-backend") and {identity(requirement) for requirement in requires} == {identity(requirement) for requirement in trusted.get("build-system", {}).get("requires", [])} and {identity(requirement) for requirement in constraints} == {identity(requirement) for requirement in trusted.get("tool", {}).get("uv", {}).get("build-constraint-dependencies", [])} and {identity(requirement) for requirement in reviewed} == {identity(requirement) for requirement in trusted.get("dependency-groups", {}).get("build", [])}, "Use only the public PyPI registry, reviewed artifacts, the exact editable root project, and locked reviewed build requirements in uv.lock"'

- name: Require published minimums for direct security updates
if: >-
github.event_name == 'pull_request'
&& github.event.pull_request.user.login == 'dependabot[bot]'
&& contains(github.event.pull_request.head.ref, 'python-security')
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
# Bash 3.2 does not apply errexit to failed [[ ... ]] guards.
[[ "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]] || exit 1
origin="$(git remote get-url origin)"
[[ "$origin" == "https://github.com/openai/openai-python" || "$origin" == "https://github.com/openai/openai-python.git" ]] || exit 1
git fetch --no-tags --depth=1 origin "$BASE_SHA"
git show "$BASE_SHA:scripts/check-dependency-security.py" | python -I -

- name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: '0.12.5'
enable-cache: true

- name: Verify dependency groups and committed lock
run: |
uv run --no-project --python '>=3.11' python -c 'import pathlib, tomllib; project = tomllib.loads(pathlib.Path("pyproject.toml").read_text()); assert project["dependency-groups"]["dev"] and "dev" in project["tool"]["uv"]["default-groups"], "Keep the uv development dependency group enabled"'
uv run --no-project --python '>=3.11' python -c 'import pathlib, tomllib; project = tomllib.loads(pathlib.Path("pyproject.toml").read_text()); text = pathlib.Path("uv.lock").read_text(); lock = tomllib.loads(text); version = project["project"]["version"]; assert next(package["version"] for package in lock["package"] if package["name"] == "openai") == version and "version = " + chr(34) + version + chr(34) + " # x-release-please-version" in text, "Preserve the uv.lock release-please marker and project version"'
uv lock --check

lint:
timeout-minutes: 10
name: lint
runs-on: ubuntu-latest
needs: dependency-locks
if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand All @@ -44,6 +92,7 @@ jobs:
run: ./scripts/lint

build:
needs: dependency-locks
if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork
timeout-minutes: 10
name: build
Expand All @@ -65,7 +114,9 @@ jobs:
run: uv sync --locked --all-extras

- name: Run build
run: ./scripts/build
run: |
external_packages="$(python -c 'import importlib, re, sys; parser = importlib.import_module("tomllib" if sys.version_info >= (3, 11) else "tomli"); packages = parser.loads(open("uv.lock").read())["package"]; roots = [package for package in packages if package.get("source") == {"editable": "."}]; external = [package for package in packages if package not in roots]; assert len(roots) == 1 and roots[0].get("name") == "openai" and external and all(package.get("source") == {"registry": "https://pypi.org/simple"} and isinstance(package.get("name"), str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", package["name"]) for package in external), "Refuse malformed external build package restrictions"; denied = sorted({re.sub(r"[-_.]+", "-", package["name"]).lower() for package in external}); assert denied and "openai" not in denied, "Refuse incomplete external build package restrictions"; print(" ".join(denied))')"
env -u UV_NO_BUILD UV_NO_BUILD_PACKAGE="$external_packages" ./scripts/build

- name: Validate Python version metadata
run: uv run --locked --all-extras python scripts/utils/validate-python-version-wheel.py
Expand Down Expand Up @@ -96,6 +147,7 @@ jobs:
timeout-minutes: 15
name: test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
needs: dependency-locks
if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork
strategy:
fail-fast: false
Expand Down Expand Up @@ -132,6 +184,7 @@ jobs:
timeout-minutes: 20
name: test (HTTPX2)
runs-on: ubuntu-latest
needs: dependency-locks
if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand Down Expand Up @@ -167,6 +220,7 @@ jobs:
name: examples
environment: ci
runs-on: ubuntu-latest
needs: dependency-locks
if: github.repository == 'openai/openai-python' && github.event_name == 'push' && github.ref == 'refs/heads/main'

steps:
Expand Down Expand Up @@ -209,8 +263,11 @@ jobs:
timeout-minutes: 20
name: compatibility (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
needs: dependency-locks
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
continue-on-error: ${{ matrix.experimental }}
env:
UV_NO_BUILD: ${{ matrix.experimental && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '0' || '1' }}
strategy:
fail-fast: false
matrix:
Expand Down
Loading
Loading