Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-id for openSUSE:Factory 
checked in at 2026-03-30 18:29:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-id (Old)
 and      /work/SRC/openSUSE:Factory/.python-id.new.1999 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-id"

Mon Mar 30 18:29:53 2026 rev:3 rq:1343437 version:1.6.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-id/python-id.changes      2025-01-27 
20:52:45.276452583 +0100
+++ /work/SRC/openSUSE:Factory/.python-id.new.1999/python-id.changes    
2026-03-30 18:30:06.728109819 +0200
@@ -1,0 +2,10 @@
+Sun Mar 29 13:50:15 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 1.6.1:
+  * Fixed an issue where the correct audience was not being
+    requested for GitHub identities
+  * CircleCI: default to --root-issuer when generating OIDC Token
+  * Drop dependency on `requests` in favor of underlying
+    `urllib3`
+
+-------------------------------------------------------------------

Old:
----
  id-1.5.0.tar.gz

New:
----
  id-1.6.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-id.spec ++++++
--- /var/tmp/diff_new_pack.JNMi85/_old  2026-03-30 18:30:07.408138081 +0200
+++ /var/tmp/diff_new_pack.JNMi85/_new  2026-03-30 18:30:07.416138415 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-id
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,24 +18,24 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-id
-Version:        1.5.0
+Version:        1.6.1
 Release:        0
 Summary:        A tool for generating OIDC identities
 License:        Apache-2.0
 URL:            https://github.com/di/id
 Source:         
https://github.com/di/id/archive/v%{version}.tar.gz#/id-%{version}.tar.gz
-BuildRequires:  python-rpm-macros
 BuildRequires:  %{python_module flit-core >= 3.2}
 BuildRequires:  %{python_module pip}
+BuildRequires:  python-rpm-macros
 # SECTION test requirements
-BuildRequires:  %{python_module requests}
+BuildRequires:  %{python_module urllib3}
 BuildRequires:  %{python_module coverage}
 BuildRequires:  %{python_module pretend}
-BuildRequires:  %{python_module pytest}
 BuildRequires:  %{python_module pytest-cov}
+BuildRequires:  %{python_module pytest}
 # /SECTION
 BuildRequires:  fdupes
-Requires:       python-requests
+Requires:       python-urllib3
 BuildArch:      noarch
 %python_subpackages
 

++++++ id-1.5.0.tar.gz -> id-1.6.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/.github/dependabot.yml 
new/id-1.6.1/.github/dependabot.yml
--- old/id-1.5.0/.github/dependabot.yml 2024-12-04 20:50:43.000000000 +0100
+++ new/id-1.6.1/.github/dependabot.yml 2026-02-04 16:11:48.000000000 +0100
@@ -4,12 +4,20 @@
   - package-ecosystem: pip
     directory: /
     schedule:
-      interval: daily
+      interval: monthly
+    cooldown:
+      default-days: 7
+    groups:
+      pip:
+        patterns:
+          - "*"
 
   - package-ecosystem: github-actions
     directory: /
     schedule:
-      interval: daily
+      interval: monthly
+    cooldown:
+      default-days: 7
     open-pull-requests-limit: 99
     rebase-strategy: "disabled"
     groups:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/.github/workflows/ci.yml 
new/id-1.6.1/.github/workflows/ci.yml
--- old/id-1.5.0/.github/workflows/ci.yml       2024-12-04 20:50:43.000000000 
+0100
+++ new/id-1.6.1/.github/workflows/ci.yml       2026-02-04 16:11:48.000000000 
+0100
@@ -6,34 +6,35 @@
       - main
   pull_request:
   schedule:
-    - cron: '0 12 * * *'
+    - cron: "0 12 * * *"
+  workflow_dispatch:
+
+permissions: {}
 
 jobs:
-  test:
-    permissions:
-      # Needed to access the workflow's OIDC identity.
-      id-token: write
+  unit-test:
     strategy:
       matrix:
         conf:
-          - { py: "3.8", os: "ubuntu-latest" }
           - { py: "3.9", os: "ubuntu-latest" }
           - { py: "3.10", os: "ubuntu-latest" }
           - { py: "3.11", os: "ubuntu-latest" }
           - { py: "3.12", os: "ubuntu-latest" }
           - { py: "3.13", os: "ubuntu-latest" }
+          - { py: "3.14", os: "ubuntu-latest" }
+          - { py: "pypy3.11", os: "ubuntu-latest" }
           # NOTE: We only test Windows and macOS on the latest Python;
           # these primarily exist to ensure that we don't accidentally
           # introduce Linux-isms into the development tooling.
-          - { py: "3.13", os: "windows-latest" }
-          - { py: "3.13", os: "macos-latest" }
+          - { py: "3.14", os: "windows-latest" }
+          - { py: "3.14", os: "macos-latest" }
     runs-on: ${{ matrix.conf.os }}
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 
v4.2.2
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
         with:
           persist-credentials: false
 
-      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b
+      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
         with:
           python-version: ${{ matrix.conf.py }}
           cache: "pip"
@@ -45,11 +46,37 @@
       - name: test
         run: make test TEST_ARGS="-vv --showlocals"
 
+  github-actions-test:
+    if: github.event_name != 'pull_request' || 
github.event.pull_request.head.repo.fork == false
+    runs-on: ubuntu-latest
+    permissions:
+      id-token: write # for OIDC
+    steps:
+      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 
v6.0.1
+        with:
+          persist-credentials: false
+
+      - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
+        with:
+          python-version: "3.x"
+          cache: "pip"
+          cache-dependency-path: pyproject.toml
+
+      - name: deps
+        run: pip install .
+
+      - name: Test id with GitHub Actions OIDC
+        shell: python
+        run: |
+          import id
+          assert id.detect_credential("my-audience")
+
   all-tests-pass:
     if: always()
 
     needs:
-    - test
+      - unit-test
+      - github-actions-test
 
     runs-on: ubuntu-latest
 
@@ -58,3 +85,4 @@
         uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # 
v1.2.2
         with:
           jobs: ${{ toJSON(needs) }}
+          allowed-skips: github-actions-test
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/.github/workflows/lint.yml 
new/id-1.6.1/.github/workflows/lint.yml
--- old/id-1.5.0/.github/workflows/lint.yml     2024-12-04 20:50:43.000000000 
+0100
+++ new/id-1.6.1/.github/workflows/lint.yml     2026-02-04 16:11:48.000000000 
+0100
@@ -6,18 +6,20 @@
       - main
   pull_request:
 
+permissions: {}
+
 jobs:
   lint:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 
v4.2.2
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
         with:
           persist-credentials: false
 
       # NOTE: We intentionally lint against our minimum supported Python.
-      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b
+      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
         with:
-          python-version: "3.8"
+          python-version: "3.9"
           cache: "pip"
           cache-dependency-path: pyproject.toml
 
@@ -30,15 +32,15 @@
   check-readme:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 
v4.2.2
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
         with:
           persist-credentials: false
 
       # NOTE: We intentionally check `--help` rendering against our minimum 
Python,
       # since it changes slightly between Python versions.
-      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b
+      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
         with:
-          python-version: "3.8"
+          python-version: "3.9"
           cache: "pip"
           cache-dependency-path: pyproject.toml
 
@@ -51,7 +53,7 @@
   licenses:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 
v4.2.2
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
         with:
           persist-credentials: false
 
@@ -68,9 +70,9 @@
     if: always()
 
     needs:
-    - lint
-    - check-readme
-    - licenses
+      - lint
+      - check-readme
+      - licenses
 
     runs-on: ubuntu-latest
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/.github/workflows/release.yml 
new/id-1.6.1/.github/workflows/release.yml
--- old/id-1.5.0/.github/workflows/release.yml  2024-12-04 20:50:43.000000000 
+0100
+++ new/id-1.6.1/.github/workflows/release.yml  2026-02-04 16:11:48.000000000 
+0100
@@ -5,7 +5,7 @@
     types:
       - published
 
-permissions:  # added using https://github.com/step-security/secure-workflows
+permissions: # added using https://github.com/step-security/secure-workflows
   contents: read
 
 jobs:
@@ -15,11 +15,11 @@
     outputs:
       hashes: ${{ steps.hash.outputs.hashes }}
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 
v4.2.2
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
         with:
           persist-credentials: false
 
-      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b
+      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
         with:
           python-version: "3.x"
           cache: "pip"
@@ -41,7 +41,7 @@
           echo "hashes=$(sha256sum ./dist/* | base64 -w0)" >> $GITHUB_OUTPUT
 
       - name: Upload built packages
-        uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 
# v4.4.3
+        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f 
# v6.0.0
         with:
           name: built-packages
           path: ./dist/
@@ -51,12 +51,12 @@
     needs: [build]
     name: Generate build provenance
     permissions:
-      actions: read   # To read the workflow path.
+      actions: read # To read the workflow path.
       id-token: write # To sign the provenance.
       contents: write # To add assets to a release.
     # Currently this action needs to be referred by tag. More details at:
     # 
https://github.com/slsa-framework/slsa-github-generator#verification-of-provenance
-    uses: 
slsa-framework/slsa-github-generator/.github/workflows/[email protected]
+    uses: 
slsa-framework/slsa-github-generator/.github/workflows/[email protected]
     with:
       provenance-name: provenance-id-${{ github.event.release.tag_name 
}}.intoto.jsonl
       base64-subjects: "${{ needs.build.outputs.hashes }}"
@@ -69,10 +69,10 @@
       id-token: write # To upload via OIDC + generate attestations.
     steps:
       - name: Download artifacts directories # goes to current working 
directory
-        uses: 
actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: 
actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
 
       - name: publish
-        uses: 
pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2
+        uses: 
pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
         with:
           packages-dir: built-packages/
           attestations: true
@@ -85,13 +85,13 @@
       contents: write
     steps:
       - name: Download artifacts directories # goes to current working 
directory
-        uses: 
actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: 
actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
 
       - name: Upload artifacts to GitHub
         # Confusingly, this action also supports updating releases, not
         # just creating them. This is what we want here, since we've manually
         # created the release that triggered the action.
-        uses: 
softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
+        uses: 
softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
         with:
           files: |
             built-packages/*
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/.github/workflows/scorecards-analysis.yml 
new/id-1.6.1/.github/workflows/scorecards-analysis.yml
--- old/id-1.5.0/.github/workflows/scorecards-analysis.yml      2024-12-04 
20:50:43.000000000 +0100
+++ new/id-1.6.1/.github/workflows/scorecards-analysis.yml      2026-02-04 
16:11:48.000000000 +0100
@@ -4,9 +4,9 @@
   workflow_dispatch: # Manual
   branch_protection_rule:
   schedule:
-    - cron: '30 4 * * 0'
+    - cron: "30 4 * * 0"
   push:
-    branches: [ main ]
+    branches: [main]
 
 permissions: {} # Remove all job-level permissions.
 
@@ -23,12 +23,12 @@
       id-token: write
     steps:
       - name: "Checkout code"
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 
v4.2.2
+        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
         with:
           persist-credentials: false
 
       - name: "Run analysis"
-        uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # 
v2.4.0
+        uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # 
v2.4.3
         with:
           results_file: results.sarif
           results_format: sarif
@@ -43,7 +43,7 @@
 
       # Upload the results as artifacts (optional).
       - name: "Upload artifact"
-        uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 
# v2.3.1
+        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f 
# v6.0.0
         with:
           name: SARIF file
           path: results.sarif
@@ -51,6 +51,6 @@
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: 
github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # 
v3.27.6
+        uses: 
github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # 
v4.31.9
         with:
           sarif_file: results.sarif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/CHANGELOG.md new/id-1.6.1/CHANGELOG.md
--- old/id-1.5.0/CHANGELOG.md   2024-12-04 20:50:43.000000000 +0100
+++ new/id-1.6.1/CHANGELOG.md   2026-02-04 16:11:48.000000000 +0100
@@ -6,6 +6,23 @@
 
 ## [Unreleased]
 
+
+## [1.6.1]
+
+### Fixed
+
+* Fixed an issue where the correct audience was not being requested for GitHub
+  identities ([#441](https://github.com/di/id/issues/441))
+
+## [1.6.0]
+
+### Changed
+
+* CircleCI: default to --root-issuer when generating OIDC Token 
([#438](https://github.com/di/id/pull/438))
+
+* Drop dependency on `requests` in favor of underlying
+  `urllib3` ([#333](https://github.com/di/id/pull/333))
+
 ## [1.5.0]
 
 ### Changed
@@ -60,7 +77,9 @@
 * Initial split from https://github.com/sigstore/sigstore-python
 
 <!--Release URLs -->
-[Unreleased]: https://github.com/di/id/compare/v1.5.0...HEAD
+[Unreleased]: https://github.com/di/id/compare/v1.6.1...HEAD
+[1.6.1]: https://github.com/di/id/compare/v1.6.0...v1.6.1
+[1.6.0]: https://github.com/di/id/compare/v1.5.0...v1.6.0
 [1.5.0]: https://github.com/di/id/compare/v1.4.0...v1.5.0
 [1.4.0]: https://github.com/di/id/compare/v1.3.0...v1.4.0
 [1.3.0]: https://github.com/di/id/compare/v1.2.1...v1.3.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/id/__init__.py new/id-1.6.1/id/__init__.py
--- old/id-1.5.0/id/__init__.py 2024-12-04 20:50:43.000000000 +0100
+++ new/id-1.6.1/id/__init__.py 2026-02-04 16:11:48.000000000 +0100
@@ -19,9 +19,11 @@
 from __future__ import annotations
 
 import base64
+import binascii
+import json
 from typing import Callable
 
-__version__ = "1.5.0"
+__version__ = "1.6.1"
 
 
 class IdentityError(Exception):
@@ -50,6 +52,26 @@
     pass
 
 
+def _validate_credential(credential: str, audience: str) -> None:
+    # Decode credential to verify it roughly looks like a token and contains
+    # the correct audience
+    try:
+        _, payload, _ = credential.split(".")
+        decoded_payload = base64.urlsafe_b64decode(payload + 
"==").decode("utf-8")
+        payload_json = json.loads(decoded_payload)
+    except (ValueError, binascii.Error, json.decoder.JSONDecodeError) as e:
+        raise AmbientCredentialError("Malformed token") from e
+
+    if not isinstance(payload_json, dict):
+        raise AmbientCredentialError("Malformed token payload (JWT is not a 
JSON object)")
+    if "aud" not in payload_json:
+        raise AmbientCredentialError("Malformed token payload (audience claim 
is missing)")
+    if payload_json["aud"] != audience:
+        raise AmbientCredentialError(
+            f"Token audience claim mismatch (expected {audience}, got 
{payload_json['aud']})"
+        )
+
+
 def detect_credential(audience: str) -> str | None:
     """
     Try each ambient credential detector, returning the first one to succeed
@@ -76,6 +98,7 @@
     for detector in detectors:
         credential = detector(audience)
         if credential is not None:
+            _validate_credential(credential, audience)
             return credential
     return None
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/id/_internal/oidc/ambient.py 
new/id-1.6.1/id/_internal/oidc/ambient.py
--- old/id-1.5.0/id/_internal/oidc/ambient.py   2024-12-04 20:50:43.000000000 
+0100
+++ new/id-1.6.1/id/_internal/oidc/ambient.py   2026-02-04 16:11:48.000000000 
+0100
@@ -24,8 +24,10 @@
 import re
 import shutil
 import subprocess  # nosec B404
+from typing import Any, TextIO
+from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
 
-import requests
+import urllib3
 
 from ... import AmbientCredentialError, GitHubOidcPermissionCredentialError
 
@@ -45,6 +47,32 @@
 _env_var_regex = re.compile(r"[^A-Z0-9_]|^[^A-Z_]")
 
 
+def _request(
+    method: str,
+    url: str,
+    *,
+    fields: dict[str, str] | None = None,
+    **kwargs: Any,
+) -> urllib3.BaseHTTPResponse:
+    """request wrapper that handles adding query parameters to URLs that may 
already have them"""
+    _encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"}
+    if method.upper() in _encode_url_methods and fields:
+        url_parts = list(urlparse(url))
+        query = dict(parse_qsl(url_parts[4]))
+        query.update(fields)
+        url_parts[4] = urlencode(query)
+
+        url = urlunparse(url_parts)
+        fields = None
+
+    return urllib3.request(method, url, fields=fields, **kwargs)
+
+
+# Wrap `open` for testing purposes
+def _open(filename: str) -> TextIO:
+    return open(filename)
+
+
 def detect_github(audience: str) -> str | None:
     """
     Detect and return a GitHub Actions ambient OIDC credential.
@@ -77,22 +105,23 @@
         )
 
     logger.debug("GitHub: requesting OIDC token")
-    resp = requests.get(
-        req_url,
-        params={"audience": audience},
-        headers={"Authorization": f"bearer {req_token}"},
-        timeout=30,
-    )
+
     try:
-        resp.raise_for_status()
-    except requests.HTTPError as http_error:
-        raise AmbientCredentialError(
-            f"GitHub: OIDC token request failed (code={resp.status_code}, "
-            f"body={resp.content.decode()!r})"
-        ) from http_error
-    except requests.Timeout:
+        resp = _request(
+            "GET",
+            req_url,
+            fields={"audience": audience},
+            headers={"Authorization": f"bearer {req_token}"},
+            timeout=30,
+        )
+    except urllib3.exceptions.MaxRetryError:
         raise AmbientCredentialError("GitHub: OIDC token request timed out")
 
+    if resp.status != 200:
+        raise AmbientCredentialError(
+            f"GitHub: OIDC token request failed (code={resp.status}, 
body={resp.data.decode()!r})"
+        )
+
     try:
         body = resp.json()
         value = body["value"]
@@ -122,47 +151,49 @@
         logger.debug("GCP: GOOGLE_SERVICE_ACCOUNT_NAME set; attempting 
impersonation")
 
         logger.debug("GCP: requesting access token")
-        resp = requests.get(
-            _GCP_TOKEN_REQUEST_URL,
-            params={"scopes": 
"https://www.googleapis.com/auth/cloud-platform"},
-            headers={"Metadata-Flavor": "Google"},
-            timeout=30,
-        )
+
         try:
-            resp.raise_for_status()
-        except requests.HTTPError as http_error:
-            raise AmbientCredentialError(
-                f"GCP: access token request failed (code={resp.status_code}, "
-                f"body={resp.content.decode()!r})"
-            ) from http_error
-        except requests.Timeout:
+            resp = _request(
+                "GET",
+                _GCP_TOKEN_REQUEST_URL,
+                fields={"scopes": 
"https://www.googleapis.com/auth/cloud-platform"},
+                headers={"Metadata-Flavor": "Google"},
+                timeout=30,
+            )
+        except urllib3.exceptions.MaxRetryError:
             raise AmbientCredentialError("GCP: access token request timed out")
 
+        if resp.status != 200:
+            raise AmbientCredentialError(
+                f"GCP: access token request failed (code={resp.status}, "
+                f"body={resp.data.decode()!r})"
+            )
+
         access_token = resp.json().get("access_token")
 
         if not access_token:
             raise AmbientCredentialError("GCP: access token missing from 
response")
 
-        resp = requests.post(
-            _GCP_GENERATEIDTOKEN_REQUEST_URL.format(service_account_name),
-            json={"audience": audience, "includeEmail": True},
-            headers={
-                "Authorization": f"Bearer {access_token}",
-            },
-            timeout=30,
-        )
-
         logger.debug("GCP: requesting OIDC token")
+
         try:
-            resp.raise_for_status()
-        except requests.HTTPError as http_error:
-            raise AmbientCredentialError(
-                f"GCP: OIDC token request failed (code={resp.status_code}, "
-                f"body={resp.content.decode()!r})"
-            ) from http_error
-        except requests.Timeout:
+            resp = _request(
+                "POST",
+                _GCP_GENERATEIDTOKEN_REQUEST_URL.format(service_account_name),
+                json={"audience": audience, "includeEmail": True},
+                headers={
+                    "Authorization": f"Bearer {access_token}",
+                },
+                timeout=30,
+            )
+        except urllib3.exceptions.MaxRetryError:
             raise AmbientCredentialError("GCP: OIDC token request timed out")
 
+        if resp.status != 200:
+            raise AmbientCredentialError(
+                f"GCP: OIDC token request failed (code={resp.status}, 
body={resp.data.decode()!r})"
+            )
+
         oidc_token: str = resp.json().get("token")
 
         if not oidc_token:
@@ -175,7 +206,7 @@
         logger.debug("GCP: GOOGLE_SERVICE_ACCOUNT_NAME not set; skipping 
impersonation")
 
         try:
-            with open(_GCP_PRODUCT_NAME_FILE) as f:
+            with _open(_GCP_PRODUCT_NAME_FILE) as f:
                 name = f.read().strip()
         except OSError:
             logger.debug("GCP: environment doesn't have GCP product name file; 
giving up")
@@ -186,25 +217,25 @@
             return None
 
         logger.debug("GCP: requesting OIDC token")
-        resp = requests.get(
-            _GCP_IDENTITY_REQUEST_URL,
-            params={"audience": audience, "format": "full"},
-            headers={"Metadata-Flavor": "Google"},
-            timeout=30,
-        )
 
         try:
-            resp.raise_for_status()
-        except requests.HTTPError as http_error:
-            raise AmbientCredentialError(
-                f"GCP: OIDC token request failed (code={resp.status_code}, "
-                f"body={resp.content.decode()!r})"
-            ) from http_error
-        except requests.Timeout:
+            resp = _request(
+                "GET",
+                _GCP_IDENTITY_REQUEST_URL,
+                fields={"audience": audience, "format": "full"},
+                headers={"Metadata-Flavor": "Google"},
+                timeout=30,
+            )
+        except urllib3.exceptions.MaxRetryError:
             raise AmbientCredentialError("GCP: OIDC token request timed out")
 
+        if resp.status != 200:
+            raise AmbientCredentialError(
+                f"GCP: OIDC token request failed (code={resp.status}, 
body={resp.data.decode()!r})"
+            )
+
         logger.debug("GCP: successfully requested OIDC token")
-        return resp.text
+        return resp.data.decode()
 
 
 def detect_buildkite(audience: str) -> str | None:
@@ -293,7 +324,7 @@
     return token
 
 
-def detect_circleci(audience: str) -> str | None:
+def detect_circleci(audience: str, root_issuer: bool = True) -> str | None:
     """
     Detect and return a CircleCI ambient OIDC credential.
 
@@ -312,17 +343,21 @@
     if shutil.which("circleci") is None:
         raise AmbientCredentialError("CircleCI: could not find `circleci` in 
the environment")
 
-    # See NOTE on `detect_buildkite` for why we silence these warnings.
     payload = json.dumps({"aud": audience})
+    cmd = ["circleci", "run", "oidc", "get", "--claims", payload]
+    if root_issuer:
+        cmd.append("--root-issuer")
+
+    # See NOTE on `detect_buildkite` for why we silence these warnings.
     process = subprocess.run(  # nosec B603, B607
-        ["circleci", "run", "oidc", "get", "--claims", payload],
+        cmd,
         capture_output=True,
         text=True,
     )
 
     if process.returncode != 0:
         raise AmbientCredentialError(
-            f"CircleCI: the `circleci` tool encountered an error: 
{process.stdout}"
+            f"CircleCI: the `circleci` tool encountered an error: 
{process.stderr}"
         )
 
     return process.stdout.strip()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/id/py.typed new/id-1.6.1/id/py.typed
--- old/id-1.5.0/id/py.typed    1970-01-01 01:00:00.000000000 +0100
+++ new/id-1.6.1/id/py.typed    2026-02-04 16:11:48.000000000 +0100
@@ -0,0 +1,4 @@
+`id` is a project providing helpers for making OIDC identities.
+This PEP 561 marker file exists to let the type checkers know that this project
+has type annotations declared. Additionally, this is useful
+for type-checking our own tests since they make corresponding imports.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/pyproject.toml new/id-1.6.1/pyproject.toml
--- old/id-1.5.0/pyproject.toml 2024-12-04 20:50:43.000000000 +0100
+++ new/id-1.6.1/pyproject.toml 2026-02-04 16:11:48.000000000 +0100
@@ -12,19 +12,19 @@
   "License :: OSI Approved :: Apache Software License",
   "Programming Language :: Python :: 3 :: Only",
   "Programming Language :: Python :: 3",
-  "Programming Language :: Python :: 3.8",
   "Programming Language :: Python :: 3.9",
   "Programming Language :: Python :: 3.10",
   "Programming Language :: Python :: 3.11",
   "Programming Language :: Python :: 3.12",
   "Programming Language :: Python :: 3.13",
+  "Programming Language :: Python :: 3.14",
   "Development Status :: 5 - Production/Stable",
   "Intended Audience :: Developers",
   "Topic :: Security",
   "Topic :: Security :: Cryptography",
 ]
-dependencies = ["requests"]
-requires-python = ">=3.8"
+dependencies = ["urllib3 >= 2, < 3"]
+requires-python = ">=3.9"
 
 [project.urls]
 Homepage = "https://pypi.org/project/id/";
@@ -42,8 +42,7 @@
   "mypy",
   # NOTE(ww): ruff is under active development, so we pin conservatively here
   # and let Dependabot periodically perform this update.
-  "ruff < 0.8.2",
-  "types-requests",
+  "ruff < 0.14.15",
 ]
 dev = ["build", "bump >= 1.3.2", "id[test,lint]"]
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/test/unit/internal/oidc/gha_token.txt 
new/id-1.6.1/test/unit/internal/oidc/gha_token.txt
--- old/id-1.5.0/test/unit/internal/oidc/gha_token.txt  1970-01-01 
01:00:00.000000000 +0100
+++ new/id-1.6.1/test/unit/internal/oidc/gha_token.txt  2026-02-04 
16:11:48.000000000 +0100
@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4ODI2YjE3LTZhMzAtNWY5Yi1iMTY5LThiZWI4MjAyZjcyMyIsInR5cCI6IkpXVCIsIng1dCI6InlrTmFZNHFNX3RhNGsyVGdaT0NFWUxrY1lsQSJ9.eyJhY3RvciI6ImdpdGh1Yi1hY3Rpb25zW2JvdF0iLCJhY3Rvcl9pZCI6IjQxODk4MjgyIiwiYXVkIjoic2lnc3RvcmUiLCJiYXNlX3JlZiI6IiIsImNoZWNrX3J1bl9pZCI6IjYyMTI1NTYxNzA2IiwiZXZlbnRfbmFtZSI6IndvcmtmbG93X2Rpc3BhdGNoIiwiZXhwIjoxNzY5OTQxODU5LCJoZWFkX3JlZiI6IiIsImlhdCI6MTc2OTk0MTU1OSwiaXNzIjoiaHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImpvYl93b3JrZmxvd19yZWYiOiJzaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4iLCJqb2Jfd29ya2Zsb3dfc2hhIjoiOGMxMzUxNzcyMWQ2YWUxMWNhYzM1N2U1OGI5YzgxZTk4OGRjODZlNCIsImp0aSI6Ijg5NzA2ZWUzLTRmYmItNDY2MS1hMzU3LWQxMmE3OGZmYWI1OCIsIm5iZiI6MTc2OTk0MTI1OSwicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVmX3Byb3RlY3RlZCI6InRydWUiLCJyZWZfdHlwZSI6ImJyYW5jaCIsInJlcG9zaXRvcnkiOiJzaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vy
 
b3VzLXB1YmxpYy1vaWRjLWJlYWNvbiIsInJlcG9zaXRvcnlfaWQiOiI2MzI1OTY4OTciLCJyZXBvc2l0b3J5X293bmVyIjoic2lnc3RvcmUtY29uZm9ybWFuY2UiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiMTMxODA0NTYzIiwicmVwb3NpdG9yeV92aXNpYmlsaXR5IjoicHVibGljIiwicnVuX2F0dGVtcHQiOiIxIiwicnVuX2lkIjoiMjE1NjEyNDQ2OTciLCJydW5fbnVtYmVyIjoiMzA2Njk4IiwicnVubmVyX2Vudmlyb25tZW50IjoiZ2l0aHViLWhvc3RlZCIsInNoYSI6IjhjMTM1MTc3MjFkNmFlMTFjYWMzNTdlNThiOWM4MWU5ODhkYzg2ZTQiLCJzdWIiOiJyZXBvOnNpZ3N0b3JlLWNvbmZvcm1hbmNlL2V4dHJlbWVseS1kYW5nZXJvdXMtcHVibGljLW9pZGMtYmVhY29uOnJlZjpyZWZzL2hlYWRzL21haW4iLCJ3b3JrZmxvdyI6IkV4dHJlbWVseSBkYW5nZXJvdXMgT0lEQyBiZWFjb24iLCJ3b3JrZmxvd19yZWYiOiJzaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4iLCJ3b3JrZmxvd19zaGEiOiI4YzEzNTE3NzIxZDZhZTExY2FjMzU3ZTU4YjljODFlOTg4ZGM4NmU0In0.3ZAoeVzyxsaVNAda9nTLWANuOYUfjkGZXjZDwjBdxuURNHBxsygoaa5qlOcs-n0ApTIAZAFcTL2PcG_ig1FncIEAqKplYbTEWo1MP8-J-hqJ73ehBUIJ5_cUhO0-j
 
WDzVCCQuqFlkXRLGfNZoh20SvQhv0G23v31MPvrn41oGcB-iNTcMv_ECrBjqAKgb1rPHxWq7cq5jscLdrBJwPoW5fLY6AEyijgWWRBkps7mPCRnRSaadLCzJ3Juh0TGGxIw1pl0H5KAOeHG3NqMWJFBW9sJGZDQ-iAXG4CfLYYtdj-E_lAvZohbh0-vWeMcEITggfzqjEppElSM2tp_OWkeSQ
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/id-1.5.0/test/unit/internal/oidc/test_ambient.py 
new/id-1.6.1/test/unit/internal/oidc/test_ambient.py
--- old/id-1.5.0/test/unit/internal/oidc/test_ambient.py        2024-12-04 
20:50:43.000000000 +0100
+++ new/id-1.6.1/test/unit/internal/oidc/test_ambient.py        2026-02-04 
16:11:48.000000000 +0100
@@ -13,14 +13,22 @@
 # limitations under the License.
 
 import json
+from pathlib import Path
 
 import pretend
 import pytest
-from requests import HTTPError, Timeout
 
 from id import detect_credential
 from id._internal.oidc import ambient
 
+HERE = Path(__file__).parent
+
+
+# example from Github Actions (audience of the token is "sigstore")
+_GHA_TOKEN = (HERE / "gha_token.txt").read_text().strip()
+# request URL example from Github Actions: note the query params already in 
the URL
+_GHA_TOKEN_REQUEST_URL = 
"https://run-actions-3-azure-eastus.actions.githubusercontent.com/64//idtoken/918f5315-f823-4b74-ae16-8fc423e48661/0b77e920-7dce-5419-aca2-996d3c2116b7?api-version=2.0";
+
 
 def test_detect_credential_none(monkeypatch):
     detect_none = pretend.call_recorder(lambda audience: None)
@@ -31,10 +39,32 @@
 
 
 def test_detect_credential(monkeypatch):
-    detect_github = pretend.call_recorder(lambda audience: "fakejwt")
+    detect_github = pretend.call_recorder(lambda audience: _GHA_TOKEN)
     monkeypatch.setattr(ambient, "detect_github", detect_github)
 
-    assert detect_credential("some-audience") == "fakejwt"
+    assert detect_credential("sigstore") == _GHA_TOKEN
+
+
+def test_detect_credential_audience_mismatch(monkeypatch):
+    detect_github = pretend.call_recorder(lambda audience: _GHA_TOKEN)
+    monkeypatch.setattr(ambient, "detect_github", detect_github)
+
+    with pytest.raises(
+        ambient.AmbientCredentialError,
+        match=r"Token audience claim mismatch \(expected my-audience, got 
sigstore\)",
+    ):
+        detect_credential("my-audience")
+
+
+def test_detect_credential_malformed_token(monkeypatch):
+    detect_github = pretend.call_recorder(lambda audience: 
"header.payload.sig")
+    monkeypatch.setattr(ambient, "detect_github", detect_github)
+
+    with pytest.raises(
+        ambient.AmbientCredentialError,
+        match="Malformed token",
+    ):
+        detect_credential("my-audience")
 
 
 def test_detect_github_bad_env(monkeypatch):
@@ -54,7 +84,7 @@
 def test_detect_github_bad_request_token(monkeypatch):
     monkeypatch.setenv("GITHUB_ACTIONS", "true")
     monkeypatch.delenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", raising=False)
-    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "fakeurl")
+    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", _GHA_TOKEN_REQUEST_URL)
 
     logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
     monkeypatch.setattr(ambient, "logger", logger)
@@ -90,25 +120,25 @@
 def test_detect_github_request_fails(monkeypatch):
     monkeypatch.setenv("GITHUB_ACTIONS", "true")
     monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "faketoken")
-    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "fakeurl")
+    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", _GHA_TOKEN_REQUEST_URL)
 
     resp = pretend.stub(
-        raise_for_status=pretend.raiser(HTTPError),
-        status_code=999,
-        content=b"something",
+        status=999,
+        data=b"something",
     )
-    requests = pretend.stub(get=pretend.call_recorder(lambda url, **kw: resp), 
HTTPError=HTTPError)
-    monkeypatch.setattr(ambient, "requests", requests)
+    u3 = pretend.stub(request=pretend.call_recorder(lambda meth, url, **kw: 
resp))
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
         match=r"GitHub: OIDC token request failed \(code=999, 
body='something'\)",
     ):
         ambient.detect_github("some-audience")
-    assert requests.get.calls == [
+    assert u3.request.calls == [
         pretend.call(
-            "fakeurl",
-            params={"audience": "some-audience"},
+            "GET",
+            f"{_GHA_TOKEN_REQUEST_URL}&audience=some-audience",
+            fields=None,
             headers={"Authorization": "bearer faketoken"},
             timeout=30,
         )
@@ -118,49 +148,40 @@
 def test_detect_github_request_timeout(monkeypatch):
     monkeypatch.setenv("GITHUB_ACTIONS", "true")
     monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "faketoken")
-    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "fakeurl")
+    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", _GHA_TOKEN_REQUEST_URL)
 
-    resp = pretend.stub(raise_for_status=pretend.raiser(Timeout))
-    requests = pretend.stub(
-        get=pretend.call_recorder(lambda url, **kw: resp),
-        HTTPError=HTTPError,
-        Timeout=Timeout,
+    u3 = pretend.stub(
+        request=pretend.raiser(ValueError), 
exceptions=pretend.stub(MaxRetryError=ValueError)
     )
-    monkeypatch.setattr(ambient, "requests", requests)
+
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
         match=r"GitHub: OIDC token request timed out",
     ):
         ambient.detect_github("some-audience")
-    assert requests.get.calls == [
-        pretend.call(
-            "fakeurl",
-            params={"audience": "some-audience"},
-            headers={"Authorization": "bearer faketoken"},
-            timeout=30,
-        )
-    ]
 
 
 def test_detect_github_invalid_json_payload(monkeypatch):
     monkeypatch.setenv("GITHUB_ACTIONS", "true")
     monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "faketoken")
-    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "fakeurl")
+    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", _GHA_TOKEN_REQUEST_URL)
 
-    resp = pretend.stub(raise_for_status=lambda: None, 
json=pretend.raiser(json.JSONDecodeError))
-    requests = pretend.stub(get=pretend.call_recorder(lambda url, **kw: resp))
-    monkeypatch.setattr(ambient, "requests", requests)
+    resp = pretend.stub(status=200, json=pretend.raiser(json.JSONDecodeError))
+    request = pretend.call_recorder(lambda meth, url, **kw: resp)
+    monkeypatch.setattr(ambient.urllib3, "request", request)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
         match="GitHub: malformed or incomplete JSON",
     ):
         ambient.detect_github("some-audience")
-    assert requests.get.calls == [
+    assert request.calls == [
         pretend.call(
-            "fakeurl",
-            params={"audience": "some-audience"},
+            "GET",
+            f"{_GHA_TOKEN_REQUEST_URL}&audience=some-audience",
+            fields=None,
             headers={"Authorization": "bearer faketoken"},
             timeout=30,
         )
@@ -171,21 +192,22 @@
 def test_detect_github_bad_payload(monkeypatch, payload):
     monkeypatch.setenv("GITHUB_ACTIONS", "true")
     monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "faketoken")
-    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "fakeurl")
+    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", _GHA_TOKEN_REQUEST_URL)
 
-    resp = pretend.stub(raise_for_status=lambda: None, 
json=pretend.call_recorder(lambda: payload))
-    requests = pretend.stub(get=pretend.call_recorder(lambda url, **kw: resp))
-    monkeypatch.setattr(ambient, "requests", requests)
+    resp = pretend.stub(status=200, json=pretend.call_recorder(lambda: 
payload))
+    u3 = pretend.stub(request=pretend.call_recorder(lambda meth, url, **kw: 
resp))
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
         match="GitHub: malformed or incomplete JSON",
     ):
         ambient.detect_github("some-audience")
-    assert requests.get.calls == [
+    assert u3.request.calls == [
         pretend.call(
-            "fakeurl",
-            params={"audience": "some-audience"},
+            "GET",
+            f"{_GHA_TOKEN_REQUEST_URL}&audience=some-audience",
+            fields=None,
             headers={"Authorization": "bearer faketoken"},
             timeout=30,
         )
@@ -196,20 +218,21 @@
 def test_detect_github(monkeypatch):
     monkeypatch.setenv("GITHUB_ACTIONS", "true")
     monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "faketoken")
-    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "fakeurl")
+    monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_URL", _GHA_TOKEN_REQUEST_URL)
 
     resp = pretend.stub(
-        raise_for_status=lambda: None,
+        status=200,
         json=pretend.call_recorder(lambda: {"value": "fakejwt"}),
     )
-    requests = pretend.stub(get=pretend.call_recorder(lambda url, **kw: resp))
-    monkeypatch.setattr(ambient, "requests", requests)
+    u3 = pretend.stub(request=pretend.call_recorder(lambda meth, url, **kw: 
resp))
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     assert ambient.detect_github("some-audience") == "fakejwt"
-    assert requests.get.calls == [
+    assert u3.request.calls == [
         pretend.call(
-            "fakeurl",
-            params={"audience": "some-audience"},
+            "GET",
+            f"{_GHA_TOKEN_REQUEST_URL}&audience=some-audience",
+            fields=None,
             headers={"Authorization": "bearer faketoken"},
             timeout=30,
         )
@@ -223,13 +246,9 @@
     logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
     monkeypatch.setattr(ambient, "logger", logger)
 
-    resp = pretend.stub(
-        raise_for_status=pretend.raiser(HTTPError),
-        status_code=999,
-        content=b"something",
-    )
-    requests = pretend.stub(get=pretend.call_recorder(lambda url, **kw: resp), 
HTTPError=HTTPError)
-    monkeypatch.setattr(ambient, "requests", requests)
+    resp = pretend.stub(status=999, data=b"something")
+    u3 = pretend.stub(request=pretend.call_recorder(lambda meth, url, **kw: 
resp))
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
@@ -250,13 +269,11 @@
     logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
     monkeypatch.setattr(ambient, "logger", logger)
 
-    resp = pretend.stub(raise_for_status=pretend.raiser(Timeout))
-    requests = pretend.stub(
-        get=pretend.call_recorder(lambda url, **kw: resp),
-        HTTPError=HTTPError,
-        Timeout=Timeout,
+    u3 = pretend.stub(
+        request=pretend.raiser(ValueError), 
exceptions=pretend.stub(MaxRetryError=ValueError)
     )
-    monkeypatch.setattr(ambient, "requests", requests)
+
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
@@ -277,9 +294,9 @@
     logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
     monkeypatch.setattr(ambient, "logger", logger)
 
-    resp = pretend.stub(raise_for_status=lambda: None, json=lambda: {})
-    requests = pretend.stub(get=pretend.call_recorder(lambda url, **kw: resp))
-    monkeypatch.setattr(ambient, "requests", requests)
+    resp = pretend.stub(status=200, json=lambda: {})
+    u3 = pretend.stub(request=pretend.call_recorder(lambda meth, url, **kw: 
resp))
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
@@ -301,20 +318,22 @@
     monkeypatch.setattr(ambient, "logger", logger)
 
     access_token = pretend.stub()
-    get_resp = pretend.stub(
-        raise_for_status=lambda: None, json=lambda: {"access_token": 
access_token}
-    )
+    get_resp = pretend.stub(status=200, json=lambda: {"access_token": 
access_token})
     post_resp = pretend.stub(
-        raise_for_status=pretend.raiser(HTTPError),
-        status_code=999,
-        content=b"something",
-    )
-    requests = pretend.stub(
-        get=pretend.call_recorder(lambda url, **kw: get_resp),
-        post=pretend.call_recorder(lambda url, **kw: post_resp),
-        HTTPError=HTTPError,
+        status=999,
+        data=b"something",
     )
-    monkeypatch.setattr(ambient, "requests", requests)
+
+    def _request(meth, *a, **kw):
+        if meth == "GET":
+            return get_resp
+        elif meth == "POST":
+            return post_resp
+        else:
+            assert False
+
+    u3 = pretend.stub(request=_request)
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
@@ -337,17 +356,18 @@
     monkeypatch.setattr(ambient, "logger", logger)
 
     access_token = pretend.stub()
-    get_resp = pretend.stub(
-        raise_for_status=lambda: None, json=lambda: {"access_token": 
access_token}
-    )
-    post_resp = pretend.stub(raise_for_status=pretend.raiser(Timeout))
-    requests = pretend.stub(
-        get=pretend.call_recorder(lambda url, **kw: get_resp),
-        post=pretend.call_recorder(lambda url, **kw: post_resp),
-        HTTPError=HTTPError,
-        Timeout=Timeout,
-    )
-    monkeypatch.setattr(ambient, "requests", requests)
+    get_resp = pretend.stub(status=200, json=lambda: {"access_token": 
access_token})
+
+    def _request(meth, *a, **kw):
+        if meth == "GET":
+            return get_resp
+        elif meth == "POST":
+            raise ValueError
+        else:
+            assert False
+
+    u3 = pretend.stub(request=_request, 
exceptions=pretend.stub(MaxRetryError=ValueError))
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
@@ -370,16 +390,19 @@
     monkeypatch.setattr(ambient, "logger", logger)
 
     access_token = pretend.stub()
-    get_resp = pretend.stub(
-        raise_for_status=lambda: None, json=lambda: {"access_token": 
access_token}
-    )
-    post_resp = pretend.stub(raise_for_status=lambda: None, json=lambda: {})
-    requests = pretend.stub(
-        get=pretend.call_recorder(lambda url, **kw: get_resp),
-        post=pretend.call_recorder(lambda url, **kw: post_resp),
-        HTTPError=HTTPError,
-    )
-    monkeypatch.setattr(ambient, "requests", requests)
+    get_resp = pretend.stub(status=200, json=lambda: {"access_token": 
access_token})
+    post_resp = pretend.stub(status=200, json=lambda: {})
+
+    def _request(meth, *a, **kw):
+        if meth == "GET":
+            return get_resp
+        elif meth == "POST":
+            return post_resp
+        else:
+            assert False
+
+    u3 = pretend.stub(request=_request)
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
@@ -403,16 +426,19 @@
 
     access_token = pretend.stub()
     oidc_token = pretend.stub()
-    get_resp = pretend.stub(
-        raise_for_status=lambda: None, json=lambda: {"access_token": 
access_token}
-    )
-    post_resp = pretend.stub(raise_for_status=lambda: None, json=lambda: 
{"token": oidc_token})
-    requests = pretend.stub(
-        get=pretend.call_recorder(lambda url, **kw: get_resp),
-        post=pretend.call_recorder(lambda url, **kw: post_resp),
-        HTTPError=HTTPError,
-    )
-    monkeypatch.setattr(ambient, "requests", requests)
+    get_resp = pretend.stub(status=200, json=lambda: {"access_token": 
access_token})
+    post_resp = pretend.stub(status=200, json=lambda: {"token": oidc_token})
+
+    def _request(meth, *a, **kw):
+        if meth == "GET":
+            return get_resp
+        elif meth == "POST":
+            return post_resp
+        else:
+            assert False
+
+    u3 = pretend.stub(request=_request)
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     assert ambient.detect_gcp("some-audience") == oidc_token
 
@@ -427,7 +453,7 @@
 
 def test_gcp_bad_env(monkeypatch):
     oserror = pretend.raiser(OSError)
-    monkeypatch.setitem(ambient.__builtins__, "open", oserror)  # type: ignore
+    monkeypatch.setattr(ambient, "_open", oserror)  # type: ignore
 
     logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
     monkeypatch.setattr(ambient, "logger", logger)
@@ -445,7 +471,7 @@
         __enter__=lambda *a: pretend.stub(read=lambda: "Unsupported Product"),
         __exit__=lambda *a: None,
     )
-    monkeypatch.setitem(ambient.__builtins__, "open", lambda fn: stub_file)  # 
type: ignore
+    monkeypatch.setattr(ambient, "_open", lambda fn: stub_file)  # type: ignore
 
     logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
     monkeypatch.setattr(ambient, "logger", logger)
@@ -466,25 +492,25 @@
         __enter__=lambda *a: pretend.stub(read=lambda: "Google"),
         __exit__=lambda *a: None,
     )
-    monkeypatch.setitem(ambient.__builtins__, "open", lambda fn: stub_file)  # 
type: ignore
+    monkeypatch.setattr(ambient, "_open", lambda fn: stub_file)  # type: ignore
 
     resp = pretend.stub(
-        raise_for_status=pretend.raiser(HTTPError),
-        status_code=999,
-        content=b"something",
+        status=999,
+        data=b"something",
     )
-    requests = pretend.stub(get=pretend.call_recorder(lambda url, **kw: resp), 
HTTPError=HTTPError)
-    monkeypatch.setattr(ambient, "requests", requests)
+    u3 = pretend.stub(request=pretend.call_recorder(lambda meth, url, **kw: 
resp))
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
         match=r"GCP: OIDC token request failed \(code=999, body='something'\)",
     ):
         ambient.detect_gcp("some-audience")
-    assert requests.get.calls == [
+    assert u3.request.calls == [
         pretend.call(
-            ambient._GCP_IDENTITY_REQUEST_URL,
-            params={"audience": "some-audience", "format": "full"},
+            "GET",
+            
f"{ambient._GCP_IDENTITY_REQUEST_URL}?audience=some-audience&format=full",
+            fields=None,
             headers={"Metadata-Flavor": "Google"},
             timeout=30,
         )
@@ -496,29 +522,18 @@
         __enter__=lambda *a: pretend.stub(read=lambda: "Google"),
         __exit__=lambda *a: None,
     )
-    monkeypatch.setitem(ambient.__builtins__, "open", lambda fn: stub_file)  # 
type: ignore
+    monkeypatch.setattr(ambient, "_open", lambda fn: stub_file)  # type: ignore
 
-    resp = pretend.stub(raise_for_status=pretend.raiser(Timeout))
-    requests = pretend.stub(
-        get=pretend.call_recorder(lambda url, **kw: resp),
-        HTTPError=HTTPError,
-        Timeout=Timeout,
+    u3 = pretend.stub(
+        request=pretend.raiser(ValueError), 
exceptions=pretend.stub(MaxRetryError=ValueError)
     )
-    monkeypatch.setattr(ambient, "requests", requests)
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     with pytest.raises(
         ambient.AmbientCredentialError,
         match=r"GCP: OIDC token request timed out",
     ):
         ambient.detect_gcp("some-audience")
-    assert requests.get.calls == [
-        pretend.call(
-            ambient._GCP_IDENTITY_REQUEST_URL,
-            params={"audience": "some-audience", "format": "full"},
-            headers={"Metadata-Flavor": "Google"},
-            timeout=30,
-        )
-    ]
 
 
 @pytest.mark.parametrize("product_name", ("Google", "Google Compute Engine"))
@@ -527,23 +542,24 @@
         __enter__=lambda *a: pretend.stub(read=lambda: product_name),
         __exit__=lambda *a: None,
     )
-    monkeypatch.setitem(ambient.__builtins__, "open", lambda fn: stub_file)  # 
type: ignore
+    monkeypatch.setattr(ambient, "_open", lambda fn: stub_file)  # type: ignore
 
     logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
     monkeypatch.setattr(ambient, "logger", logger)
 
     resp = pretend.stub(
-        raise_for_status=lambda: None,
-        text="fakejwt",
+        status=200,
+        data=b"fakejwt",
     )
-    requests = pretend.stub(get=pretend.call_recorder(lambda url, **kw: resp))
-    monkeypatch.setattr(ambient, "requests", requests)
+    u3 = pretend.stub(request=pretend.call_recorder(lambda meth, url, **kw: 
resp))
+    monkeypatch.setattr(ambient, "urllib3", u3)
 
     assert ambient.detect_gcp("some-audience") == "fakejwt"
-    assert requests.get.calls == [
+    assert u3.request.calls == [
         pretend.call(
-            ambient._GCP_IDENTITY_REQUEST_URL,
-            params={"audience": "some-audience", "format": "full"},
+            "GET",
+            
f"{ambient._GCP_IDENTITY_REQUEST_URL}?audience=some-audience&format=full",
+            fields=None,
             headers={"Metadata-Flavor": "Google"},
             timeout=30,
         )
@@ -721,7 +737,8 @@
     assert shutil.which.calls == [pretend.call("circleci")]
 
 
-def test_circleci_circlecli_error(monkeypatch):
[email protected]("root_issuer", [True, False])
+def test_circleci_circlecli_error(monkeypatch, root_issuer):
     monkeypatch.setenv("CIRCLECI", "true")
 
     # Mock out the `which` call to show that we have a `circleci` in our 
`PATH`.
@@ -731,29 +748,34 @@
     # Mock out `run` call to emulate getting a non-zero return code from the 
`circleci`.
     resp = pretend.stub(
         returncode=-1,
-        stdout="mock error message",
+        stderr="mock error message",
     )
     subprocess = pretend.stub(run=pretend.call_recorder(lambda run_args, **kw: 
resp), PIPE=None)
     monkeypatch.setattr(ambient, "subprocess", subprocess)
+
     payload = json.dumps({"aud": "some-audience"})
+    expected_cmd = ["circleci", "run", "oidc", "get", "--claims", payload]
+    if root_issuer:
+        expected_cmd.append("--root-issuer")
 
     with pytest.raises(
         ambient.AmbientCredentialError,
         match=r"CircleCI: the `circleci` tool encountered an error: mock error 
message",
     ):
-        ambient.detect_circleci("some-audience")
+        ambient.detect_circleci("some-audience", root_issuer)
 
     assert shutil.which.calls == [pretend.call("circleci")]
     assert subprocess.run.calls == [
         pretend.call(
-            ["circleci", "run", "oidc", "get", "--claims", payload],
+            expected_cmd,
             capture_output=True,
             text=True,
         )
     ]
 
 
-def test_circleci(monkeypatch):
[email protected]("root_issuer", [True, False])
+def test_circleci(monkeypatch, root_issuer):
     monkeypatch.setenv("CIRCLECI", "true")
 
     # Mock out the `which` call to show that we have a `circleci` in our 
`PATH`.
@@ -767,13 +789,17 @@
     )
     subprocess = pretend.stub(run=pretend.call_recorder(lambda run_args, **kw: 
resp), PIPE=None)
     monkeypatch.setattr(ambient, "subprocess", subprocess)
+
     payload = json.dumps({"aud": "some-audience"})
+    expected_cmd = ["circleci", "run", "oidc", "get", "--claims", payload]
+    if root_issuer:
+        expected_cmd.append("--root-issuer")
 
-    assert ambient.detect_circleci("some-audience") == "fakejwt"
+    assert ambient.detect_circleci("some-audience", root_issuer) == "fakejwt"
     assert shutil.which.calls == [pretend.call("circleci")]
     assert subprocess.run.calls == [
         pretend.call(
-            ["circleci", "run", "oidc", "get", "--claims", payload],
+            expected_cmd,
             capture_output=True,
             text=True,
         )

Reply via email to