From cd96e0348e23055e9005770e7c013a58945ecc8b Mon Sep 17 00:00:00 2001
From: IaroslavTitov <iaro@pulumi.com>
Date: Wed, 19 Mar 2025 12:03:22 -0600
Subject: [PATCH 1/3] Adding workspace/account logic for python

---
 CHANGELOG_PENDING.md                          |  2 +
 sdk/python/pulumi_esc_sdk/esc_client.py       |  9 ++
 sdk/python/pulumi_esc_sdk/workspace.py        | 89 +++++++++++++++++++
 sdk/python/pulumi_esc_sdk/workspace_models.py | 41 +++++++++
 sdk/python/test/test_esc_api.py               |  2 +-
 .../test/test_pulumi_home/credentials.json    | 25 ++++++
 .../.esc/credentials.json                     |  3 +
 .../test_pulumi_home_esc/credentials.json     | 25 ++++++
 sdk/python/test/test_workspace_accounts.py    | 51 +++++++++++
 9 files changed, 246 insertions(+), 1 deletion(-)
 create mode 100644 sdk/python/pulumi_esc_sdk/workspace.py
 create mode 100644 sdk/python/pulumi_esc_sdk/workspace_models.py
 create mode 100644 sdk/python/test/test_pulumi_home/credentials.json
 create mode 100644 sdk/python/test/test_pulumi_home_esc/.esc/credentials.json
 create mode 100644 sdk/python/test/test_pulumi_home_esc/credentials.json
 create mode 100644 sdk/python/test/test_workspace_accounts.py

diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md
index 14cbe53..0006413 100644
--- a/CHANGELOG_PENDING.md
+++ b/CHANGELOG_PENDING.md
@@ -4,6 +4,8 @@
   - All SDK now automatically read in configuration environment variables
   - Go SDK also automatically picks up configuration from CLI Pulumi accounts
   [#76](https://github.com/pulumi/esc-sdk/pull/76)
+- Adding CLI Pulumi accounts loading logic into Python SDK
+  [#81](https://github.com/pulumi/esc-sdk/pull/81)
 
 ### Bug Fixes
 
diff --git a/sdk/python/pulumi_esc_sdk/esc_client.py b/sdk/python/pulumi_esc_sdk/esc_client.py
index 59543e9..c4c3e10 100644
--- a/sdk/python/pulumi_esc_sdk/esc_client.py
+++ b/sdk/python/pulumi_esc_sdk/esc_client.py
@@ -12,6 +12,7 @@
 import yaml
 import os
 from urllib.parse import urlparse, urlunparse
+import pulumi_esc_sdk.workspace as workspace
 
 
 class EscClient:
@@ -441,6 +442,14 @@ def default_config(host=None,
         access_token = os.getenv("PULUMI_ACCESS_TOKEN")
     if not host:
         host = os.getenv("PULUMI_BACKEND_URL")
+    
+    account, backend_url = workspace.get_current_account(False)
+
+    if not access_token:
+        access_token = account.accessToken if account else None
+    if not host:
+        host = backend_url
+
     return configuration.Configuration(
         host=host,
         access_token=access_token,
diff --git a/sdk/python/pulumi_esc_sdk/workspace.py b/sdk/python/pulumi_esc_sdk/workspace.py
new file mode 100644
index 0000000..39684f0
--- /dev/null
+++ b/sdk/python/pulumi_esc_sdk/workspace.py
@@ -0,0 +1,89 @@
+# Copyright 2025, Pulumi Corporation.  All rights reserved.
+
+"""Recreating Pulumi workspace and account logic for python SDK."""
+
+from dataclasses import dataclass, field
+import json
+import os
+import pathlib
+from typing import Optional
+
+from pulumi_esc_sdk.workspace_models import Account, Credentials
+
+def get_pulumi_home_dir() -> str:
+    """
+    Returns the path of the '.pulumi' folder where Pulumi puts its artifacts.
+    """
+    # Allow the folder we use to be overridden by an environment variable
+    dir_env = os.getenv("PULUMI_HOME")
+    if dir_env:
+        return dir_env
+
+    # Otherwise, use the current user's home dir + .pulumi
+    home_dir = pathlib.Path.home()
+
+    return str(home_dir / ".pulumi")
+
+def get_pulumi_path(*elem: str) -> str:
+    """
+    Returns the path to a file or directory under the '.pulumi' folder.
+    It joins the path of the '.pulumi' folder with elements passed as arguments.
+    """
+    home_dir = get_pulumi_home_dir()
+    return str(pathlib.Path(home_dir).joinpath(*elem))
+
+def get_esc_bookkeeping_dir() -> str:
+    """
+    Returns the path of the '.esc' folder inside Pulumi home dir.
+    """
+    return get_pulumi_path(".esc")
+
+def append_creds_file(dir: str) -> str:
+    """
+    Returns the path to the esc credentials file on disk.
+    """
+    return str(pathlib.Path(dir) / "credentials.json")
+
+def get_esc_current_account_name() -> Optional[str]:
+    """
+    Returns the current account name from the ESC credentials file.
+    """
+    creds_file = append_creds_file(get_esc_bookkeeping_dir())
+    try:
+        with open(creds_file, 'r') as f:
+            data = json.loads(f.read())
+            return data['name']
+    except FileNotFoundError:
+        return None
+    except Exception as e:
+        print(f"An unexpected error occured: {e}")
+        return None
+
+def get_stored_credentials() -> Credentials:
+    """
+    Reads and parses credentials from the Pulumi credentials file.
+    """
+    creds_file = append_creds_file(get_pulumi_path())
+    try:
+        with open(creds_file, 'r') as f:
+            data = f.read()
+            creds = Credentials.from_json(data)
+            return creds
+    except FileNotFoundError:
+        return None
+    except Exception as e:
+        print(f"An unexpected error occured: {e}")
+        return None
+    
+def get_current_account(shared) -> tuple[Account, str]:
+    """
+    Gets current account values from credentials file.
+    """
+    backend_url = get_esc_current_account_name()
+    pulumi_credentials = get_stored_credentials()
+    if not pulumi_credentials:
+        return None, None
+    if backend_url is None or shared:
+        backend_url = pulumi_credentials.current
+    pulumi_account = pulumi_credentials.accounts[backend_url]
+    return pulumi_account, backend_url
diff --git a/sdk/python/pulumi_esc_sdk/workspace_models.py b/sdk/python/pulumi_esc_sdk/workspace_models.py
new file mode 100644
index 0000000..04738d4
--- /dev/null
+++ b/sdk/python/pulumi_esc_sdk/workspace_models.py
@@ -0,0 +1,41 @@
+# Copyright 2025, Pulumi Corporation.  All rights reserved.
+
+"""Pulumi workspace models."""
+
+from dataclasses import dataclass, field
+import json
+from typing import Dict, List, Optional
+
+@dataclass
+class Account:
+    accessToken: str
+    username: str
+    organizations: List[str]
+
+    @classmethod
+    def from_json(self, data: dict):
+        return self(
+            accessToken=data.get("accessToken"),
+            username=data.get("username"),
+            organizations=data.get("organizations", []),
+        )
+
+@dataclass
+class Credentials:
+    current: str
+    accessTokens: Dict[str, str]
+    accounts: Dict[str, Account]
+
+    @classmethod
+    def from_json(self, dataJson: str):
+        data = json.loads(dataJson)
+        accounts: Dict[str, Account] = {}
+        accounts_dict = data.get("accounts")
+        if accounts_dict:
+            for account_name, account_data in accounts_dict.items():
+                accounts[account_name] = Account.from_json(account_data)
+        return self(
+            current=data.get("current"),
+            accessTokens=data.get("accessTokens"),
+            accounts=accounts
+        )
\ No newline at end of file
diff --git a/sdk/python/test/test_esc_api.py b/sdk/python/test/test_esc_api.py
index d3f5e8b..1eddfd6 100644
--- a/sdk/python/test/test_esc_api.py
+++ b/sdk/python/test/test_esc_api.py
@@ -16,7 +16,7 @@ class TestEscApi(unittest.TestCase):
     """EscApi unit test stubs"""
 
     def setUp(self) -> None:
-        self.orgName = "pulumi"
+        self.orgName = os.getenv("PULUMI_ORG")
         self.assertIsNotNone(self.orgName, "PULUMI_ORG must be set")
 
         self.client = esc.esc_client.default_client()
diff --git a/sdk/python/test/test_pulumi_home/credentials.json b/sdk/python/test/test_pulumi_home/credentials.json
new file mode 100644
index 0000000..1af2f2c
--- /dev/null
+++ b/sdk/python/test/test_pulumi_home/credentials.json
@@ -0,0 +1,25 @@
+{
+    "current": "https://api.moolumi.com",
+    "accessTokens": {
+        "https://api.moolumi.com": "pul-fake-token-moo",
+        "https://api.boolumi.com": "pul-fake-token-boo"
+    },
+    "accounts": {
+        "https://api.moolumi.com": {
+            "accessToken": "pul-fake-token-moo",
+            "username": "moolumipus",
+            "organizations": [
+                "moolumipus-org"
+            ],
+            "lastValidatedAt": "2025-03-17T11:19:28.392525488-06:00"
+        },
+        "https://api.boolumi.com": {
+            "accessToken": "pul-fake-token-boo",
+            "username": "boolumipus",
+            "organizations": [
+                "boolumipus-org2"
+            ],
+            "lastValidatedAt": "2026-03-17T11:19:28.392525488-06:00"
+        }
+    }
+}
\ No newline at end of file
diff --git a/sdk/python/test/test_pulumi_home_esc/.esc/credentials.json b/sdk/python/test/test_pulumi_home_esc/.esc/credentials.json
new file mode 100644
index 0000000..0dbca97
--- /dev/null
+++ b/sdk/python/test/test_pulumi_home_esc/.esc/credentials.json
@@ -0,0 +1,3 @@
+{
+    "name": "https://api.boolumi.com"
+}
\ No newline at end of file
diff --git a/sdk/python/test/test_pulumi_home_esc/credentials.json b/sdk/python/test/test_pulumi_home_esc/credentials.json
new file mode 100644
index 0000000..1af2f2c
--- /dev/null
+++ b/sdk/python/test/test_pulumi_home_esc/credentials.json
@@ -0,0 +1,25 @@
+{
+    "current": "https://api.moolumi.com",
+    "accessTokens": {
+        "https://api.moolumi.com": "pul-fake-token-moo",
+        "https://api.boolumi.com": "pul-fake-token-boo"
+    },
+    "accounts": {
+        "https://api.moolumi.com": {
+            "accessToken": "pul-fake-token-moo",
+            "username": "moolumipus",
+            "organizations": [
+                "moolumipus-org"
+            ],
+            "lastValidatedAt": "2025-03-17T11:19:28.392525488-06:00"
+        },
+        "https://api.boolumi.com": {
+            "accessToken": "pul-fake-token-boo",
+            "username": "boolumipus",
+            "organizations": [
+                "boolumipus-org2"
+            ],
+            "lastValidatedAt": "2026-03-17T11:19:28.392525488-06:00"
+        }
+    }
+}
\ No newline at end of file
diff --git a/sdk/python/test/test_workspace_accounts.py b/sdk/python/test/test_workspace_accounts.py
new file mode 100644
index 0000000..f84fe0b
--- /dev/null
+++ b/sdk/python/test/test_workspace_accounts.py
@@ -0,0 +1,51 @@
+# coding: utf-8
+
+# Copyright 2024, Pulumi Corporation.  All rights reserved.
+
+from typing import Optional
+import unittest
+import os
+
+import pulumi_esc_sdk as esc
+
+class TestWorkspaceAccounts(unittest.TestCase):
+    """WorkspaceAccounts unit test stubs"""
+    tokenBefore: Optional[str]
+    backendBefore: Optional[str]
+    homeBefore: Optional[str]
+
+    def setUp(self) -> None:
+        self.tokenBefore = os.getenv("PULUMI_ACCESS_TOKEN")
+        self.backendBefore = os.getenv("PULUMI_BACKEND_URL")
+        self.homeBefore = os.getenv("PULUMI_HOME")
+        os.environ["PULUMI_ACCESS_TOKEN"] = ""
+        os.environ["PULUMI_BACKEND_URL"] = ""
+
+    def tearDown(self) -> None:
+        os.environ["PULUMI_ACCESS_TOKEN"] = self.tokenBefore or ''
+        os.environ["PULUMI_BACKEND_URL"] = self.backendBefore or ''
+        os.environ["PULUMI_HOME"] = self.homeBefore or ''
+
+    def test_no_creds_at_all(self):
+        os.environ["PULUMI_HOME"] = "/not_real_dir"
+        self.client = esc.esc_client.default_client()
+        self.config = self.client.esc_api.api_client.configuration
+        self.assertEqual(self.config.host, "https://api.pulumi.com/api/esc")
+        self.assertTrue('Authorization' not in self.config.api_key)
+
+    def test_just_pulumi_creds(self):
+        os.environ["PULUMI_HOME"] = os.getcwd() + "/test/test_pulumi_home"
+        self.client = esc.esc_client.default_client()
+        self.config = self.client.esc_api.api_client.configuration
+        self.assertEqual(self.config.host, "https://api.moolumi.com/api/esc")
+        self.assertTrue(self.config.api_key['Authorization'], "pul-fake-token-moo")
+
+    def test_pulumi_creds_with_esc(self):
+        os.environ["PULUMI_HOME"] = os.getcwd() + "/test/test_pulumi_home_esc"
+        self.client = esc.esc_client.default_client()
+        self.config = self.client.esc_api.api_client.configuration
+        self.assertEqual(self.config.host, "https://api.boolumi.com/api/esc")
+        self.assertTrue(self.config.api_key['Authorization'], "pul-fake-token-boo")
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file

From 0e2f959149d3420bac6199829fd9f88165c84886 Mon Sep 17 00:00:00 2001
From: IaroslavTitov <iaro@pulumi.com>
Date: Wed, 19 Mar 2025 15:33:54 -0600
Subject: [PATCH 2/3] Following up on comments

---
 .gitattributes                          |  2 ++
 sdk/python/pulumi_esc_sdk/esc_client.py | 12 ++++++------
 2 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/.gitattributes b/.gitattributes
index ed7a363..4b6536e 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -4,6 +4,8 @@ sdk/go/api_esc_test.go linguist-generated=false
 
 sdk/python/**/* linguist-generated=true
 sdk/python/pulumi_esc_sdk/esc_client.py linguist-generated=false
+sdk/python/pulumi_esc_sdk/workspace.py linguist-generated=false
+sdk/python/pulumi_esc_sdk/workspace_models.py linguist-generated=false
 sdk/python/test/**/* linguist-generated=false
 sdk/python/pyproject.toml linguist-generated=false
 
diff --git a/sdk/python/pulumi_esc_sdk/esc_client.py b/sdk/python/pulumi_esc_sdk/esc_client.py
index c4c3e10..03a6d23 100644
--- a/sdk/python/pulumi_esc_sdk/esc_client.py
+++ b/sdk/python/pulumi_esc_sdk/esc_client.py
@@ -443,12 +443,12 @@ def default_config(host=None,
     if not host:
         host = os.getenv("PULUMI_BACKEND_URL")
     
-    account, backend_url = workspace.get_current_account(False)
-
-    if not access_token:
-        access_token = account.accessToken if account else None
-    if not host:
-        host = backend_url
+    if not access_token or not host:
+        account, backend_url = workspace.get_current_account(False)
+        if not access_token:
+            access_token = account.accessToken if account else None
+        if not host:
+            host = backend_url
 
     return configuration.Configuration(
         host=host,

From e477f0d5e1d380b5af3bd5d88f1c4f4ede69eb96 Mon Sep 17 00:00:00 2001
From: IaroslavTitov <iaro@pulumi.com>
Date: Wed, 19 Mar 2025 16:45:15 -0600
Subject: [PATCH 3/3] Fixing up bad syntax based on comments

---
 CHANGELOG_PENDING.md                             |  2 +-
 sdk/python/pulumi_esc_sdk/workspace.py           | 16 ++++++++++------
 sdk/python/pulumi_esc_sdk/workspace_models.py    |  8 ++++++--
 .../test/test_pulumi_home/credentials.json       |  2 +-
 .../test_pulumi_home_esc/.esc/credentials.json   |  2 +-
 .../test/test_pulumi_home_esc/credentials.json   |  2 +-
 sdk/python/test/test_workspace_accounts.py       |  2 +-
 7 files changed, 21 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md
index 0006413..3806a0f 100644
--- a/CHANGELOG_PENDING.md
+++ b/CHANGELOG_PENDING.md
@@ -4,7 +4,7 @@
   - All SDK now automatically read in configuration environment variables
   - Go SDK also automatically picks up configuration from CLI Pulumi accounts
   [#76](https://github.com/pulumi/esc-sdk/pull/76)
-- Adding CLI Pulumi accounts loading logic into Python SDK
+- Adds support for reading credentials from disk to Python SDK
   [#81](https://github.com/pulumi/esc-sdk/pull/81)
 
 ### Bug Fixes
diff --git a/sdk/python/pulumi_esc_sdk/workspace.py b/sdk/python/pulumi_esc_sdk/workspace.py
index 39684f0..1667c20 100644
--- a/sdk/python/pulumi_esc_sdk/workspace.py
+++ b/sdk/python/pulumi_esc_sdk/workspace.py
@@ -1,6 +1,10 @@
 # Copyright 2025, Pulumi Corporation.  All rights reserved.
 
-"""Recreating Pulumi workspace and account logic for python SDK."""
+"""
+Pulumi workspace and account logic for python SDK.
+This is a partial port of ESC and Pulumi CLI code found in
+https://github.com/pulumi/esc/tree/main/cmd/esc/cli/workspace
+"""
 
 from dataclasses import dataclass, field
 import json
@@ -22,7 +26,7 @@ def get_pulumi_home_dir() -> str:
     # Otherwise, use the current user's home dir + .pulumi
     home_dir = pathlib.Path.home()
 
-    return str(home_dir / ".pulumi")
+    return str(home_dir.joinpath(".pulumi"))
 
 def get_pulumi_path(*elem: str) -> str:
     """
@@ -38,17 +42,17 @@ def get_esc_bookkeeping_dir() -> str:
     """
     return get_pulumi_path(".esc")
 
-def append_creds_file(dir: str) -> str:
+def get_path_to_creds_file(dir: str) -> str:
     """
     Returns the path to the esc credentials file on disk.
     """
-    return str(pathlib.Path(dir) / "credentials.json")
+    return str(pathlib.Path(dir).joinpath("credentials.json"))
 
 def get_esc_current_account_name() -> Optional[str]:
     """
     Returns the current account name from the ESC credentials file.
     """
-    creds_file = append_creds_file(get_esc_bookkeeping_dir())
+    creds_file = get_path_to_creds_file(get_esc_bookkeeping_dir())
     try:
         with open(creds_file, 'r') as f:
             data = json.loads(f.read())
@@ -63,7 +67,7 @@ def get_stored_credentials() -> Credentials:
     """
     Reads and parses credentials from the Pulumi credentials file.
     """
-    creds_file = append_creds_file(get_pulumi_path())
+    creds_file = get_path_to_creds_file(get_pulumi_path())
     try:
         with open(creds_file, 'r') as f:
             data = f.read()
diff --git a/sdk/python/pulumi_esc_sdk/workspace_models.py b/sdk/python/pulumi_esc_sdk/workspace_models.py
index 04738d4..e96be88 100644
--- a/sdk/python/pulumi_esc_sdk/workspace_models.py
+++ b/sdk/python/pulumi_esc_sdk/workspace_models.py
@@ -1,6 +1,10 @@
 # Copyright 2025, Pulumi Corporation.  All rights reserved.
 
-"""Pulumi workspace models."""
+"""
+Models for Pulumi workspace and account logic for python SDK.
+This is a partial port of ESC and Pulumi CLI code found in
+https://github.com/pulumi/esc/tree/main/cmd/esc/cli/workspace
+"""
 
 from dataclasses import dataclass, field
 import json
@@ -38,4 +42,4 @@ def from_json(self, dataJson: str):
             current=data.get("current"),
             accessTokens=data.get("accessTokens"),
             accounts=accounts
-        )
\ No newline at end of file
+        )
diff --git a/sdk/python/test/test_pulumi_home/credentials.json b/sdk/python/test/test_pulumi_home/credentials.json
index 1af2f2c..764cbf7 100644
--- a/sdk/python/test/test_pulumi_home/credentials.json
+++ b/sdk/python/test/test_pulumi_home/credentials.json
@@ -22,4 +22,4 @@
             "lastValidatedAt": "2026-03-17T11:19:28.392525488-06:00"
         }
     }
-}
\ No newline at end of file
+}
diff --git a/sdk/python/test/test_pulumi_home_esc/.esc/credentials.json b/sdk/python/test/test_pulumi_home_esc/.esc/credentials.json
index 0dbca97..ce9c84c 100644
--- a/sdk/python/test/test_pulumi_home_esc/.esc/credentials.json
+++ b/sdk/python/test/test_pulumi_home_esc/.esc/credentials.json
@@ -1,3 +1,3 @@
 {
     "name": "https://api.boolumi.com"
-}
\ No newline at end of file
+}
diff --git a/sdk/python/test/test_pulumi_home_esc/credentials.json b/sdk/python/test/test_pulumi_home_esc/credentials.json
index 1af2f2c..764cbf7 100644
--- a/sdk/python/test/test_pulumi_home_esc/credentials.json
+++ b/sdk/python/test/test_pulumi_home_esc/credentials.json
@@ -22,4 +22,4 @@
             "lastValidatedAt": "2026-03-17T11:19:28.392525488-06:00"
         }
     }
-}
\ No newline at end of file
+}
diff --git a/sdk/python/test/test_workspace_accounts.py b/sdk/python/test/test_workspace_accounts.py
index f84fe0b..56f4f07 100644
--- a/sdk/python/test/test_workspace_accounts.py
+++ b/sdk/python/test/test_workspace_accounts.py
@@ -48,4 +48,4 @@ def test_pulumi_creds_with_esc(self):
         self.assertTrue(self.config.api_key['Authorization'], "pul-fake-token-boo")
 
 if __name__ == '__main__':
-    unittest.main()
\ No newline at end of file
+    unittest.main()