[Python] Bindings for GcsFileSystem FromServiceAccountCredentials #50888

Description

@hampsterx

Describe the enhancement requested

GcsOptions::FromServiceAccountCredentials (cpp/src/arrow/filesystem/gcsfs.h:146) is the
one credential factory on GcsOptions with no Python binding, so a caller holding
aip/4112 service-account JSON has no way to construct a GcsFileSystem from it.

python/pyarrow/includes/libarrow_fs.pxd declares the other four (Defaults, Anonymous,
FromAccessToken, FromImpersonatedServiceAccount, lines 229-240), and the
credential-routing chain inside GcsFileSystem.__init__ (python/pyarrow/_gcsfs.pyx:103-131,
method at line 88) selects among them. No constructor keyword reaches the fifth. Line
references are against d048f71; behaviour checked on pyarrow 25.0.1.

The R bindings already have it. GcsFileSystem$create() takes json_credentials, either the
JSON itself or a path to a file holding it (r/R/filesystem.R:611-674), and routes it to
GcsOptions::FromServiceAccountCredentials (r/src/filesystem.cpp:415-417); it also rejects
json_credentials alongside access_token, which is the mutual exclusion proposed below. That
landed in #34524 (GH-34421) in March 2023. So the two bindings have complementary holes: R has
service-account JSON and lacks impersonation (#32207), Python has impersonation
(target_service_account) and lacks service-account JSON.

Two routes are open today to an application holding that JSON in memory, and each gives
something up:

  • Write it to a file and point GOOGLE_APPLICATION_CREDENTIALS at it. That puts the private
    key on disk, and it configures the process rather than the filesystem object, so two
    filesystems in one process cannot use different service accounts. The docstring on this
    function names the case it exists for: "Using this function may be useful when the json
    object is obtained from a Cloud Secret Manager or a similar service."
  • Mint an access token out of band and pass access_token + credential_token_expiration.
    Per FromAccessToken's docstring those tokens are time limited and the caller refreshes
    them manually, and the Python API offers no way to replace the token on an existing
    GcsFileSystem, so a refresh means constructing a new one.

Concrete case: omniload (panodata/omniload#233) accepts GCS service-account JSON as either a
path or a base64 blob from a secret store. Its S3 and R2 transports moved to pyarrow.fs for
the per-file open latency (on that sibling transport, 8.68 ms/file through s3fs against
3.49 ms/file through Arrow, on the reader path that opens one file object per file). GCS
stayed on gcsfs, because the swap would drop that credential mode.

Proposed shape, following the existing chain:

  • Declare FromServiceAccountCredentials in libarrow_fs.pxd beside the other four.
  • Add a GcsFileSystem.__init__ keyword taking the aip/4112 JSON as a string, in the same
    base-credential branch as anonymous and access_token and mutually exclusive with both.
    service_account_json reads more clearly than service_account_credentials, since the
    value is serialized JSON rather than a credentials object; the C++ accessor calls it
    json_credentials. It would stay composable with target_service_account by passing the
    resulting credentials to FromImpersonatedServiceAccount, as the constructor already does
    for the other base credentials.
  • Pickling would need a policy decision, and I have not assumed one. __reduce__ round-trips
    access_token today (_gcsfs.pyx:183), but a long-lived private key is a different
    proposition and declining to serialize this mode is a reasonable answer. If it should
    round-trip through the existing __reduce__ design, the pxd would also need
    GcsCredentials::json_credentials() (gcsfs.h:47), which is not declared there today but is
    already read by the R glue for its own options round-trip (r/src/filesystem.cpp:489-490).

Related: #34421 and #34524 (the R binding and its path-or-string handling), #33106 (its
docs), #32207 (the reverse gap, impersonation missing in R), #12763 (the original
[Python][C++] GCS Bindings, described there as "mostly based on AWS bindings"; I did not find
service-account JSON discussed on it), #11945 (the C++ credential types this builds on).

Does a constructor keyword fit how you would want this exposed, or would you rather it
arrive as a separate classmethod?

Component(s)

Python

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      [Python] Bindings for GcsFileSystem FromServiceAccountCredentials #50888

      Description

      @hampsterx

      Describe the enhancement requested

      GcsOptions::FromServiceAccountCredentials (cpp/src/arrow/filesystem/gcsfs.h:146) is the
      one credential factory on GcsOptions with no Python binding, so a caller holding
      aip/4112 service-account JSON has no way to construct a GcsFileSystem from it.

      python/pyarrow/includes/libarrow_fs.pxd declares the other four (Defaults, Anonymous,
      FromAccessToken, FromImpersonatedServiceAccount, lines 229-240), and the
      credential-routing chain inside GcsFileSystem.__init__ (python/pyarrow/_gcsfs.pyx:103-131,
      method at line 88) selects among them. No constructor keyword reaches the fifth. Line
      references are against d048f71; behaviour checked on pyarrow 25.0.1.

      The R bindings already have it. GcsFileSystem$create() takes json_credentials, either the
      JSON itself or a path to a file holding it (r/R/filesystem.R:611-674), and routes it to
      GcsOptions::FromServiceAccountCredentials (r/src/filesystem.cpp:415-417); it also rejects
      json_credentials alongside access_token, which is the mutual exclusion proposed below. That
      landed in #34524 (GH-34421) in March 2023. So the two bindings have complementary holes: R has
      service-account JSON and lacks impersonation (#32207), Python has impersonation
      (target_service_account) and lacks service-account JSON.

      Two routes are open today to an application holding that JSON in memory, and each gives
      something up:

      • Write it to a file and point GOOGLE_APPLICATION_CREDENTIALS at it. That puts the private
        key on disk, and it configures the process rather than the filesystem object, so two
        filesystems in one process cannot use different service accounts. The docstring on this
        function names the case it exists for: "Using this function may be useful when the json
        object is obtained from a Cloud Secret Manager or a similar service."
      • Mint an access token out of band and pass access_token + credential_token_expiration.
        Per FromAccessToken's docstring those tokens are time limited and the caller refreshes
        them manually, and the Python API offers no way to replace the token on an existing
        GcsFileSystem, so a refresh means constructing a new one.

      Concrete case: omniload (panodata/omniload#233) accepts GCS service-account JSON as either a
      path or a base64 blob from a secret store. Its S3 and R2 transports moved to pyarrow.fs for
      the per-file open latency (on that sibling transport, 8.68 ms/file through s3fs against
      3.49 ms/file through Arrow, on the reader path that opens one file object per file). GCS
      stayed on gcsfs, because the swap would drop that credential mode.

      Proposed shape, following the existing chain:

      • Declare FromServiceAccountCredentials in libarrow_fs.pxd beside the other four.
      • Add a GcsFileSystem.__init__ keyword taking the aip/4112 JSON as a string, in the same
        base-credential branch as anonymous and access_token and mutually exclusive with both.
        service_account_json reads more clearly than service_account_credentials, since the
        value is serialized JSON rather than a credentials object; the C++ accessor calls it
        json_credentials. It would stay composable with target_service_account by passing the
        resulting credentials to FromImpersonatedServiceAccount, as the constructor already does
        for the other base credentials.
      • Pickling would need a policy decision, and I have not assumed one. __reduce__ round-trips
        access_token today (_gcsfs.pyx:183), but a long-lived private key is a different
        proposition and declining to serialize this mode is a reasonable answer. If it should
        round-trip through the existing __reduce__ design, the pxd would also need
        GcsCredentials::json_credentials() (gcsfs.h:47), which is not declared there today but is
        already read by the R glue for its own options round-trip (r/src/filesystem.cpp:489-490).

      Related: #34421 and #34524 (the R binding and its path-or-string handling), #33106 (its
      docs), #32207 (the reverse gap, impersonation missing in R), #12763 (the original
      [Python][C++] GCS Bindings, described there as "mostly based on AWS bindings"; I did not find
      service-account JSON discussed on it), #11945 (the C++ credential types this builds on).

      Does a constructor keyword fit how you would want this exposed, or would you rather it
      arrive as a separate classmethod?

      Component(s)

      Python

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          [Python] Bindings for GcsFileSystem FromServiceAccountCredentials #50888

          Description

          @hampsterx

          Describe the enhancement requested

          GcsOptions::FromServiceAccountCredentials (cpp/src/arrow/filesystem/gcsfs.h:146) is the
          one credential factory on GcsOptions with no Python binding, so a caller holding
          aip/4112 service-account JSON has no way to construct a GcsFileSystem from it.

          python/pyarrow/includes/libarrow_fs.pxd declares the other four (Defaults, Anonymous,
          FromAccessToken, FromImpersonatedServiceAccount, lines 229-240), and the
          credential-routing chain inside GcsFileSystem.__init__ (python/pyarrow/_gcsfs.pyx:103-131,
          method at line 88) selects among them. No constructor keyword reaches the fifth. Line
          references are against d048f71; behaviour checked on pyarrow 25.0.1.

          The R bindings already have it. GcsFileSystem$create() takes json_credentials, either the
          JSON itself or a path to a file holding it (r/R/filesystem.R:611-674), and routes it to
          GcsOptions::FromServiceAccountCredentials (r/src/filesystem.cpp:415-417); it also rejects
          json_credentials alongside access_token, which is the mutual exclusion proposed below. That
          landed in #34524 (GH-34421) in March 2023. So the two bindings have complementary holes: R has
          service-account JSON and lacks impersonation (#32207), Python has impersonation
          (target_service_account) and lacks service-account JSON.

          Two routes are open today to an application holding that JSON in memory, and each gives
          something up:

          • Write it to a file and point GOOGLE_APPLICATION_CREDENTIALS at it. That puts the private
            key on disk, and it configures the process rather than the filesystem object, so two
            filesystems in one process cannot use different service accounts. The docstring on this
            function names the case it exists for: "Using this function may be useful when the json
            object is obtained from a Cloud Secret Manager or a similar service."
          • Mint an access token out of band and pass access_token + credential_token_expiration.
            Per FromAccessToken's docstring those tokens are time limited and the caller refreshes
            them manually, and the Python API offers no way to replace the token on an existing
            GcsFileSystem, so a refresh means constructing a new one.

          Concrete case: omniload (panodata/omniload#233) accepts GCS service-account JSON as either a
          path or a base64 blob from a secret store. Its S3 and R2 transports moved to pyarrow.fs for
          the per-file open latency (on that sibling transport, 8.68 ms/file through s3fs against
          3.49 ms/file through Arrow, on the reader path that opens one file object per file). GCS
          stayed on gcsfs, because the swap would drop that credential mode.

          Proposed shape, following the existing chain:

          • Declare FromServiceAccountCredentials in libarrow_fs.pxd beside the other four.
          • Add a GcsFileSystem.__init__ keyword taking the aip/4112 JSON as a string, in the same
            base-credential branch as anonymous and access_token and mutually exclusive with both.
            service_account_json reads more clearly than service_account_credentials, since the
            value is serialized JSON rather than a credentials object; the C++ accessor calls it
            json_credentials. It would stay composable with target_service_account by passing the
            resulting credentials to FromImpersonatedServiceAccount, as the constructor already does
            for the other base credentials.
          • Pickling would need a policy decision, and I have not assumed one. __reduce__ round-trips
            access_token today (_gcsfs.pyx:183), but a long-lived private key is a different
            proposition and declining to serialize this mode is a reasonable answer. If it should
            round-trip through the existing __reduce__ design, the pxd would also need
            GcsCredentials::json_credentials() (gcsfs.h:47), which is not declared there today but is
            already read by the R glue for its own options round-trip (r/src/filesystem.cpp:489-490).

          Related: #34421 and #34524 (the R binding and its path-or-string handling), #33106 (its
          docs), #32207 (the reverse gap, impersonation missing in R), #12763 (the original
          [Python][C++] GCS Bindings, described there as "mostly based on AWS bindings"; I did not find
          service-account JSON discussed on it), #11945 (the C++ credential types this builds on).

          Does a constructor keyword fit how you would want this exposed, or would you rather it
          arrive as a separate classmethod?

          Component(s)

          Python

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [Python] Bindings for GcsFileSystem FromServiceAccountCredentials #50888

              Description

              @hampsterx

              Describe the enhancement requested

              GcsOptions::FromServiceAccountCredentials (cpp/src/arrow/filesystem/gcsfs.h:146) is the
              one credential factory on GcsOptions with no Python binding, so a caller holding
              aip/4112 service-account JSON has no way to construct a GcsFileSystem from it.

              python/pyarrow/includes/libarrow_fs.pxd declares the other four (Defaults, Anonymous,
              FromAccessToken, FromImpersonatedServiceAccount, lines 229-240), and the
              credential-routing chain inside GcsFileSystem.__init__ (python/pyarrow/_gcsfs.pyx:103-131,
              method at line 88) selects among them. No constructor keyword reaches the fifth. Line
              references are against d048f71; behaviour checked on pyarrow 25.0.1.

              The R bindings already have it. GcsFileSystem$create() takes json_credentials, either the
              JSON itself or a path to a file holding it (r/R/filesystem.R:611-674), and routes it to
              GcsOptions::FromServiceAccountCredentials (r/src/filesystem.cpp:415-417); it also rejects
              json_credentials alongside access_token, which is the mutual exclusion proposed below. That
              landed in #34524 (GH-34421) in March 2023. So the two bindings have complementary holes: R has
              service-account JSON and lacks impersonation (#32207), Python has impersonation
              (target_service_account) and lacks service-account JSON.

              Two routes are open today to an application holding that JSON in memory, and each gives
              something up:

              • Write it to a file and point GOOGLE_APPLICATION_CREDENTIALS at it. That puts the private
                key on disk, and it configures the process rather than the filesystem object, so two
                filesystems in one process cannot use different service accounts. The docstring on this
                function names the case it exists for: "Using this function may be useful when the json
                object is obtained from a Cloud Secret Manager or a similar service."
              • Mint an access token out of band and pass access_token + credential_token_expiration.
                Per FromAccessToken's docstring those tokens are time limited and the caller refreshes
                them manually, and the Python API offers no way to replace the token on an existing
                GcsFileSystem, so a refresh means constructing a new one.

              Concrete case: omniload (panodata/omniload#233) accepts GCS service-account JSON as either a
              path or a base64 blob from a secret store. Its S3 and R2 transports moved to pyarrow.fs for
              the per-file open latency (on that sibling transport, 8.68 ms/file through s3fs against
              3.49 ms/file through Arrow, on the reader path that opens one file object per file). GCS
              stayed on gcsfs, because the swap would drop that credential mode.

              Proposed shape, following the existing chain:

              • Declare FromServiceAccountCredentials in libarrow_fs.pxd beside the other four.
              • Add a GcsFileSystem.__init__ keyword taking the aip/4112 JSON as a string, in the same
                base-credential branch as anonymous and access_token and mutually exclusive with both.
                service_account_json reads more clearly than service_account_credentials, since the
                value is serialized JSON rather than a credentials object; the C++ accessor calls it
                json_credentials. It would stay composable with target_service_account by passing the
                resulting credentials to FromImpersonatedServiceAccount, as the constructor already does
                for the other base credentials.
              • Pickling would need a policy decision, and I have not assumed one. __reduce__ round-trips
                access_token today (_gcsfs.pyx:183), but a long-lived private key is a different
                proposition and declining to serialize this mode is a reasonable answer. If it should
                round-trip through the existing __reduce__ design, the pxd would also need
                GcsCredentials::json_credentials() (gcsfs.h:47), which is not declared there today but is
                already read by the R glue for its own options round-trip (r/src/filesystem.cpp:489-490).

              Related: #34421 and #34524 (the R binding and its path-or-string handling), #33106 (its
              docs), #32207 (the reverse gap, impersonation missing in R), #12763 (the original
              [Python][C++] GCS Bindings, described there as "mostly based on AWS bindings"; I did not find
              service-account JSON discussed on it), #11945 (the C++ credential types this builds on).

              Does a constructor keyword fit how you would want this exposed, or would you rather it
              arrive as a separate classmethod?

              Component(s)

              Python

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  [Python] Bindings for GcsFileSystem FromServiceAccountCredentials #50888

                  Description

                  @hampsterx

                  Describe the enhancement requested

                  GcsOptions::FromServiceAccountCredentials (cpp/src/arrow/filesystem/gcsfs.h:146) is the
                  one credential factory on GcsOptions with no Python binding, so a caller holding
                  aip/4112 service-account JSON has no way to construct a GcsFileSystem from it.

                  python/pyarrow/includes/libarrow_fs.pxd declares the other four (Defaults, Anonymous,
                  FromAccessToken, FromImpersonatedServiceAccount, lines 229-240), and the
                  credential-routing chain inside GcsFileSystem.__init__ (python/pyarrow/_gcsfs.pyx:103-131,
                  method at line 88) selects among them. No constructor keyword reaches the fifth. Line
                  references are against d048f71; behaviour checked on pyarrow 25.0.1.

                  The R bindings already have it. GcsFileSystem$create() takes json_credentials, either the
                  JSON itself or a path to a file holding it (r/R/filesystem.R:611-674), and routes it to
                  GcsOptions::FromServiceAccountCredentials (r/src/filesystem.cpp:415-417); it also rejects
                  json_credentials alongside access_token, which is the mutual exclusion proposed below. That
                  landed in #34524 (GH-34421) in March 2023. So the two bindings have complementary holes: R has
                  service-account JSON and lacks impersonation (#32207), Python has impersonation
                  (target_service_account) and lacks service-account JSON.

                  Two routes are open today to an application holding that JSON in memory, and each gives
                  something up:

                  • Write it to a file and point GOOGLE_APPLICATION_CREDENTIALS at it. That puts the private
                    key on disk, and it configures the process rather than the filesystem object, so two
                    filesystems in one process cannot use different service accounts. The docstring on this
                    function names the case it exists for: "Using this function may be useful when the json
                    object is obtained from a Cloud Secret Manager or a similar service."
                  • Mint an access token out of band and pass access_token + credential_token_expiration.
                    Per FromAccessToken's docstring those tokens are time limited and the caller refreshes
                    them manually, and the Python API offers no way to replace the token on an existing
                    GcsFileSystem, so a refresh means constructing a new one.

                  Concrete case: omniload (panodata/omniload#233) accepts GCS service-account JSON as either a
                  path or a base64 blob from a secret store. Its S3 and R2 transports moved to pyarrow.fs for
                  the per-file open latency (on that sibling transport, 8.68 ms/file through s3fs against
                  3.49 ms/file through Arrow, on the reader path that opens one file object per file). GCS
                  stayed on gcsfs, because the swap would drop that credential mode.

                  Proposed shape, following the existing chain:

                  • Declare FromServiceAccountCredentials in libarrow_fs.pxd beside the other four.
                  • Add a GcsFileSystem.__init__ keyword taking the aip/4112 JSON as a string, in the same
                    base-credential branch as anonymous and access_token and mutually exclusive with both.
                    service_account_json reads more clearly than service_account_credentials, since the
                    value is serialized JSON rather than a credentials object; the C++ accessor calls it
                    json_credentials. It would stay composable with target_service_account by passing the
                    resulting credentials to FromImpersonatedServiceAccount, as the constructor already does
                    for the other base credentials.
                  • Pickling would need a policy decision, and I have not assumed one. __reduce__ round-trips
                    access_token today (_gcsfs.pyx:183), but a long-lived private key is a different
                    proposition and declining to serialize this mode is a reasonable answer. If it should
                    round-trip through the existing __reduce__ design, the pxd would also need
                    GcsCredentials::json_credentials() (gcsfs.h:47), which is not declared there today but is
                    already read by the R glue for its own options round-trip (r/src/filesystem.cpp:489-490).

                  Related: #34421 and #34524 (the R binding and its path-or-string handling), #33106 (its
                  docs), #32207 (the reverse gap, impersonation missing in R), #12763 (the original
                  [Python][C++] GCS Bindings, described there as "mostly based on AWS bindings"; I did not find
                  service-account JSON discussed on it), #11945 (the C++ credential types this builds on).

                  Does a constructor keyword fit how you would want this exposed, or would you rather it
                  arrive as a separate classmethod?

                  Component(s)

                  Python

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      [Python] Bindings for GcsFileSystem FromServiceAccountCredentials #50888

                      Description

                      @hampsterx

                      Describe the enhancement requested

                      GcsOptions::FromServiceAccountCredentials (cpp/src/arrow/filesystem/gcsfs.h:146) is the
                      one credential factory on GcsOptions with no Python binding, so a caller holding
                      aip/4112 service-account JSON has no way to construct a GcsFileSystem from it.

                      python/pyarrow/includes/libarrow_fs.pxd declares the other four (Defaults, Anonymous,
                      FromAccessToken, FromImpersonatedServiceAccount, lines 229-240), and the
                      credential-routing chain inside GcsFileSystem.__init__ (python/pyarrow/_gcsfs.pyx:103-131,
                      method at line 88) selects among them. No constructor keyword reaches the fifth. Line
                      references are against d048f71; behaviour checked on pyarrow 25.0.1.

                      The R bindings already have it. GcsFileSystem$create() takes json_credentials, either the
                      JSON itself or a path to a file holding it (r/R/filesystem.R:611-674), and routes it to
                      GcsOptions::FromServiceAccountCredentials (r/src/filesystem.cpp:415-417); it also rejects
                      json_credentials alongside access_token, which is the mutual exclusion proposed below. That
                      landed in #34524 (GH-34421) in March 2023. So the two bindings have complementary holes: R has
                      service-account JSON and lacks impersonation (#32207), Python has impersonation
                      (target_service_account) and lacks service-account JSON.

                      Two routes are open today to an application holding that JSON in memory, and each gives
                      something up:

                      • Write it to a file and point GOOGLE_APPLICATION_CREDENTIALS at it. That puts the private
                        key on disk, and it configures the process rather than the filesystem object, so two
                        filesystems in one process cannot use different service accounts. The docstring on this
                        function names the case it exists for: "Using this function may be useful when the json
                        object is obtained from a Cloud Secret Manager or a similar service."
                      • Mint an access token out of band and pass access_token + credential_token_expiration.
                        Per FromAccessToken's docstring those tokens are time limited and the caller refreshes
                        them manually, and the Python API offers no way to replace the token on an existing
                        GcsFileSystem, so a refresh means constructing a new one.

                      Concrete case: omniload (panodata/omniload#233) accepts GCS service-account JSON as either a
                      path or a base64 blob from a secret store. Its S3 and R2 transports moved to pyarrow.fs for
                      the per-file open latency (on that sibling transport, 8.68 ms/file through s3fs against
                      3.49 ms/file through Arrow, on the reader path that opens one file object per file). GCS
                      stayed on gcsfs, because the swap would drop that credential mode.

                      Proposed shape, following the existing chain:

                      • Declare FromServiceAccountCredentials in libarrow_fs.pxd beside the other four.
                      • Add a GcsFileSystem.__init__ keyword taking the aip/4112 JSON as a string, in the same
                        base-credential branch as anonymous and access_token and mutually exclusive with both.
                        service_account_json reads more clearly than service_account_credentials, since the
                        value is serialized JSON rather than a credentials object; the C++ accessor calls it
                        json_credentials. It would stay composable with target_service_account by passing the
                        resulting credentials to FromImpersonatedServiceAccount, as the constructor already does
                        for the other base credentials.
                      • Pickling would need a policy decision, and I have not assumed one. __reduce__ round-trips
                        access_token today (_gcsfs.pyx:183), but a long-lived private key is a different
                        proposition and declining to serialize this mode is a reasonable answer. If it should
                        round-trip through the existing __reduce__ design, the pxd would also need
                        GcsCredentials::json_credentials() (gcsfs.h:47), which is not declared there today but is
                        already read by the R glue for its own options round-trip (r/src/filesystem.cpp:489-490).

                      Related: #34421 and #34524 (the R binding and its path-or-string handling), #33106 (its
                      docs), #32207 (the reverse gap, impersonation missing in R), #12763 (the original
                      [Python][C++] GCS Bindings, described there as "mostly based on AWS bindings"; I did not find
                      service-account JSON discussed on it), #11945 (the C++ credential types this builds on).

                      Does a constructor keyword fit how you would want this exposed, or would you rather it
                      arrive as a separate classmethod?

                      Component(s)

                      Python

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          [Python] Bindings for GcsFileSystem FromServiceAccountCredentials #50888

                          Description

                          @hampsterx

                          Describe the enhancement requested

                          GcsOptions::FromServiceAccountCredentials (cpp/src/arrow/filesystem/gcsfs.h:146) is the
                          one credential factory on GcsOptions with no Python binding, so a caller holding
                          aip/4112 service-account JSON has no way to construct a GcsFileSystem from it.

                          python/pyarrow/includes/libarrow_fs.pxd declares the other four (Defaults, Anonymous,
                          FromAccessToken, FromImpersonatedServiceAccount, lines 229-240), and the
                          credential-routing chain inside GcsFileSystem.__init__ (python/pyarrow/_gcsfs.pyx:103-131,
                          method at line 88) selects among them. No constructor keyword reaches the fifth. Line
                          references are against d048f71; behaviour checked on pyarrow 25.0.1.

                          The R bindings already have it. GcsFileSystem$create() takes json_credentials, either the
                          JSON itself or a path to a file holding it (r/R/filesystem.R:611-674), and routes it to
                          GcsOptions::FromServiceAccountCredentials (r/src/filesystem.cpp:415-417); it also rejects
                          json_credentials alongside access_token, which is the mutual exclusion proposed below. That
                          landed in #34524 (GH-34421) in March 2023. So the two bindings have complementary holes: R has
                          service-account JSON and lacks impersonation (#32207), Python has impersonation
                          (target_service_account) and lacks service-account JSON.

                          Two routes are open today to an application holding that JSON in memory, and each gives
                          something up:

                          • Write it to a file and point GOOGLE_APPLICATION_CREDENTIALS at it. That puts the private
                            key on disk, and it configures the process rather than the filesystem object, so two
                            filesystems in one process cannot use different service accounts. The docstring on this
                            function names the case it exists for: "Using this function may be useful when the json
                            object is obtained from a Cloud Secret Manager or a similar service."
                          • Mint an access token out of band and pass access_token + credential_token_expiration.
                            Per FromAccessToken's docstring those tokens are time limited and the caller refreshes
                            them manually, and the Python API offers no way to replace the token on an existing
                            GcsFileSystem, so a refresh means constructing a new one.

                          Concrete case: omniload (panodata/omniload#233) accepts GCS service-account JSON as either a
                          path or a base64 blob from a secret store. Its S3 and R2 transports moved to pyarrow.fs for
                          the per-file open latency (on that sibling transport, 8.68 ms/file through s3fs against
                          3.49 ms/file through Arrow, on the reader path that opens one file object per file). GCS
                          stayed on gcsfs, because the swap would drop that credential mode.

                          Proposed shape, following the existing chain:

                          • Declare FromServiceAccountCredentials in libarrow_fs.pxd beside the other four.
                          • Add a GcsFileSystem.__init__ keyword taking the aip/4112 JSON as a string, in the same
                            base-credential branch as anonymous and access_token and mutually exclusive with both.
                            service_account_json reads more clearly than service_account_credentials, since the
                            value is serialized JSON rather than a credentials object; the C++ accessor calls it
                            json_credentials. It would stay composable with target_service_account by passing the
                            resulting credentials to FromImpersonatedServiceAccount, as the constructor already does
                            for the other base credentials.
                          • Pickling would need a policy decision, and I have not assumed one. __reduce__ round-trips
                            access_token today (_gcsfs.pyx:183), but a long-lived private key is a different
                            proposition and declining to serialize this mode is a reasonable answer. If it should
                            round-trip through the existing __reduce__ design, the pxd would also need
                            GcsCredentials::json_credentials() (gcsfs.h:47), which is not declared there today but is
                            already read by the R glue for its own options round-trip (r/src/filesystem.cpp:489-490).

                          Related: #34421 and #34524 (the R binding and its path-or-string handling), #33106 (its
                          docs), #32207 (the reverse gap, impersonation missing in R), #12763 (the original
                          [Python][C++] GCS Bindings, described there as "mostly based on AWS bindings"; I did not find
                          service-account JSON discussed on it), #11945 (the C++ credential types this builds on).

                          Does a constructor keyword fit how you would want this exposed, or would you rather it
                          arrive as a separate classmethod?

                          Component(s)

                          Python

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              [Python] Bindings for GcsFileSystem FromServiceAccountCredentials #50888

                              Description

                              @hampsterx

                              Describe the enhancement requested

                              GcsOptions::FromServiceAccountCredentials (cpp/src/arrow/filesystem/gcsfs.h:146) is the
                              one credential factory on GcsOptions with no Python binding, so a caller holding
                              aip/4112 service-account JSON has no way to construct a GcsFileSystem from it.

                              python/pyarrow/includes/libarrow_fs.pxd declares the other four (Defaults, Anonymous,
                              FromAccessToken, FromImpersonatedServiceAccount, lines 229-240), and the
                              credential-routing chain inside GcsFileSystem.__init__ (python/pyarrow/_gcsfs.pyx:103-131,
                              method at line 88) selects among them. No constructor keyword reaches the fifth. Line
                              references are against d048f71; behaviour checked on pyarrow 25.0.1.

                              The R bindings already have it. GcsFileSystem$create() takes json_credentials, either the
                              JSON itself or a path to a file holding it (r/R/filesystem.R:611-674), and routes it to
                              GcsOptions::FromServiceAccountCredentials (r/src/filesystem.cpp:415-417); it also rejects
                              json_credentials alongside access_token, which is the mutual exclusion proposed below. That
                              landed in #34524 (GH-34421) in March 2023. So the two bindings have complementary holes: R has
                              service-account JSON and lacks impersonation (#32207), Python has impersonation
                              (target_service_account) and lacks service-account JSON.

                              Two routes are open today to an application holding that JSON in memory, and each gives
                              something up:

                              • Write it to a file and point GOOGLE_APPLICATION_CREDENTIALS at it. That puts the private
                                key on disk, and it configures the process rather than the filesystem object, so two
                                filesystems in one process cannot use different service accounts. The docstring on this
                                function names the case it exists for: "Using this function may be useful when the json
                                object is obtained from a Cloud Secret Manager or a similar service."
                              • Mint an access token out of band and pass access_token + credential_token_expiration.
                                Per FromAccessToken's docstring those tokens are time limited and the caller refreshes
                                them manually, and the Python API offers no way to replace the token on an existing
                                GcsFileSystem, so a refresh means constructing a new one.

                              Concrete case: omniload (panodata/omniload#233) accepts GCS service-account JSON as either a
                              path or a base64 blob from a secret store. Its S3 and R2 transports moved to pyarrow.fs for
                              the per-file open latency (on that sibling transport, 8.68 ms/file through s3fs against
                              3.49 ms/file through Arrow, on the reader path that opens one file object per file). GCS
                              stayed on gcsfs, because the swap would drop that credential mode.

                              Proposed shape, following the existing chain:

                              • Declare FromServiceAccountCredentials in libarrow_fs.pxd beside the other four.
                              • Add a GcsFileSystem.__init__ keyword taking the aip/4112 JSON as a string, in the same
                                base-credential branch as anonymous and access_token and mutually exclusive with both.
                                service_account_json reads more clearly than service_account_credentials, since the
                                value is serialized JSON rather than a credentials object; the C++ accessor calls it
                                json_credentials. It would stay composable with target_service_account by passing the
                                resulting credentials to FromImpersonatedServiceAccount, as the constructor already does
                                for the other base credentials.
                              • Pickling would need a policy decision, and I have not assumed one. __reduce__ round-trips
                                access_token today (_gcsfs.pyx:183), but a long-lived private key is a different
                                proposition and declining to serialize this mode is a reasonable answer. If it should
                                round-trip through the existing __reduce__ design, the pxd would also need
                                GcsCredentials::json_credentials() (gcsfs.h:47), which is not declared there today but is
                                already read by the R glue for its own options round-trip (r/src/filesystem.cpp:489-490).

                              Related: #34421 and #34524 (the R binding and its path-or-string handling), #33106 (its
                              docs), #32207 (the reverse gap, impersonation missing in R), #12763 (the original
                              [Python][C++] GCS Bindings, described there as "mostly based on AWS bindings"; I did not find
                              service-account JSON discussed on it), #11945 (the C++ credential types this builds on).

                              Does a constructor keyword fit how you would want this exposed, or would you rather it
                              arrive as a separate classmethod?

                              Component(s)

                              Python

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions