From 1468619fab3aa3876f5a503e56af0c48f24623b4 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 21 Jan 2026 18:08:04 -0800 Subject: [PATCH 01/12] update: add my_org --- src/py/mat3ra/api_client/client.py | 52 +++++++++++++++++++++- src/py/mat3ra/api_client/endpoints/jobs.py | 3 +- src/py/mat3ra/api_client/models.py | 6 ++- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/py/mat3ra/api_client/client.py b/src/py/mat3ra/api_client/client.py index b9c58c0..90fcfa9 100644 --- a/src/py/mat3ra/api_client/client.py +++ b/src/py/mat3ra/api_client/client.py @@ -1,7 +1,10 @@ -from typing import Any, Optional, Tuple +import os +from typing import Any, List, Optional, Tuple +import requests from pydantic import BaseModel, ConfigDict +from .constants import ACCESS_TOKEN_ENV_VAR from .endpoints.bank_materials import BankMaterialEndpoints from .endpoints.bank_workflows import BankWorkflowEndpoints from .endpoints.jobs import JobEndpoints @@ -26,8 +29,15 @@ class APIClient(BaseModel): def model_post_init(self, __context: Any) -> None: self.my_account = Account(client=self) self.account = self.my_account + self._my_organization: Optional[Account] = None self._init_endpoints(self.timeout_seconds) + @property + def my_organization(self) -> Optional[Account]: + if self._my_organization is None: + self._my_organization = self.get_default_organization() + return self._my_organization + @classmethod def env(cls) -> APIEnv: return APIEnv.from_env() @@ -128,3 +138,43 @@ def authenticate( cls._validate_auth(auth) return cls(host=host_value, port=port_value, version=version_value, secure=secure_value, auth=auth, timeout_seconds=timeout_seconds) + + def _fetch_user_accounts(self) -> List[dict]: + access_token = self.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR) + if not access_token: + raise ValueError("Access token is required to fetch accounts") + + protocol = "https" if self.secure else "http" + port_str = f":{self.port}" if self.port not in (80, 443) else "" + url = f"{protocol}://{self.host}{port_str}/api/v1/users/me" + + response = requests.get(url, headers={"Authorization": f"Bearer {access_token}"}, timeout=30) + response.raise_for_status() + return response.json()["data"]["user"].get("accounts", []) + + def list_accounts(self) -> List[dict]: + accounts = self._fetch_user_accounts() + return [ + { + "id": acc["entity"]["_id"], + "name": acc["entity"].get("name", ""), + "type": acc["entity"].get("type", "user"), + "isDefault": acc.get("isDefault", False), + } + for acc in accounts + ] + + def get_default_organization(self) -> Optional[Account]: + accounts = self._fetch_user_accounts() + organizations = [acc for acc in accounts if acc["entity"].get("type") == "organization"] + + if not organizations: + return None + + # Try to find default organization first + for org in organizations: + if org.get("isDefault"): + return Account(client=self, account_entity_id=org["entity"]["_id"]) + + # If no default, return first organization + return Account(client=self, account_entity_id=organizations[0]["entity"]["_id"]) diff --git a/src/py/mat3ra/api_client/endpoints/jobs.py b/src/py/mat3ra/api_client/endpoints/jobs.py index e9e84dd..0001695 100644 --- a/src/py/mat3ra/api_client/endpoints/jobs.py +++ b/src/py/mat3ra/api_client/endpoints/jobs.py @@ -54,7 +54,8 @@ def terminate(self, id_): """ self.request("POST", "/".join((self.name, id_, "submit")), headers=self.headers) - def build_config(self, material_ids, workflow_id, project_id, owner_id, name, compute=None, is_multi_material=False): + def build_config(self, material_ids, workflow_id, project_id, owner_id, name, compute=None, + is_multi_material=False): """ Returns a job config based on the given parameters. diff --git a/src/py/mat3ra/api_client/models.py b/src/py/mat3ra/api_client/models.py index 290c6c9..30c2562 100644 --- a/src/py/mat3ra/api_client/models.py +++ b/src/py/mat3ra/api_client/models.py @@ -39,9 +39,12 @@ class Account(BaseModel): client: Any = Field(exclude=True, repr=False) id_cache: Optional[str] = None + account_entity_id: Optional[str] = None @property def id(self) -> str: + if self.account_entity_id: + return self.account_entity_id if self.id_cache: return self.id_cache self.id_cache = self._resolve_account_id() @@ -60,7 +63,8 @@ def _resolve_account_id(self) -> str: url = _build_base_url(self.client.host, self.client.port, self.client.secure, "/api/v1/users/me") response = requests.get(url, headers={"Authorization": f"Bearer {access_token}"}, timeout=30) response.raise_for_status() - account_id = response.json()["data"]["user"]["entity"]["defaultAccountId"] + user_data = response.json()["data"]["user"] + account_id = user_data["entity"]["defaultAccountId"] os.environ[ACCOUNT_ID_ENV_VAR] = account_id self.client.auth.account_id = account_id return account_id From 2ba0c3262f030e10e3000cb8e906e975a3edc9d6 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 21 Jan 2026 18:09:20 -0800 Subject: [PATCH 02/12] update: cleanup --- src/py/mat3ra/api_client/client.py | 6 ++---- src/py/mat3ra/api_client/models.py | 6 +----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/py/mat3ra/api_client/client.py b/src/py/mat3ra/api_client/client.py index 90fcfa9..b42c737 100644 --- a/src/py/mat3ra/api_client/client.py +++ b/src/py/mat3ra/api_client/client.py @@ -171,10 +171,8 @@ def get_default_organization(self) -> Optional[Account]: if not organizations: return None - # Try to find default organization first for org in organizations: if org.get("isDefault"): - return Account(client=self, account_entity_id=org["entity"]["_id"]) + return Account(client=self, id_cache=org["entity"]["_id"]) - # If no default, return first organization - return Account(client=self, account_entity_id=organizations[0]["entity"]["_id"]) + return Account(client=self, id_cache=organizations[0]["entity"]["_id"]) diff --git a/src/py/mat3ra/api_client/models.py b/src/py/mat3ra/api_client/models.py index 30c2562..290c6c9 100644 --- a/src/py/mat3ra/api_client/models.py +++ b/src/py/mat3ra/api_client/models.py @@ -39,12 +39,9 @@ class Account(BaseModel): client: Any = Field(exclude=True, repr=False) id_cache: Optional[str] = None - account_entity_id: Optional[str] = None @property def id(self) -> str: - if self.account_entity_id: - return self.account_entity_id if self.id_cache: return self.id_cache self.id_cache = self._resolve_account_id() @@ -63,8 +60,7 @@ def _resolve_account_id(self) -> str: url = _build_base_url(self.client.host, self.client.port, self.client.secure, "/api/v1/users/me") response = requests.get(url, headers={"Authorization": f"Bearer {access_token}"}, timeout=30) response.raise_for_status() - user_data = response.json()["data"]["user"] - account_id = user_data["entity"]["defaultAccountId"] + account_id = response.json()["data"]["user"]["entity"]["defaultAccountId"] os.environ[ACCOUNT_ID_ENV_VAR] = account_id self.client.auth.account_id = account_id return account_id From 9affdf439a132d842181101769104c5d997ec251 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 21 Jan 2026 18:19:01 -0800 Subject: [PATCH 03/12] update: add helper to get account by name/idx --- src/py/mat3ra/api_client/client.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/py/mat3ra/api_client/client.py b/src/py/mat3ra/api_client/client.py index b42c737..0ba6f37 100644 --- a/src/py/mat3ra/api_client/client.py +++ b/src/py/mat3ra/api_client/client.py @@ -1,4 +1,5 @@ import os +import re from typing import Any, List, Optional, Tuple import requests @@ -164,15 +165,36 @@ def list_accounts(self) -> List[dict]: for acc in accounts ] + def get_account(self, name: Optional[str] = None, index: Optional[int] = None) -> Account: + """Get account by name (partial regex match) or index from the list of user accounts.""" + if name is None and index is None: + raise ValueError("Either 'name' or 'index' must be provided") + + accounts = self._fetch_user_accounts() + + if index is not None: + return Account(client=self, id_cache=accounts[index]["entity"]["_id"]) + + pattern = re.compile(name, re.IGNORECASE) + matches = [acc for acc in accounts if pattern.search(acc["entity"].get("name", ""))] + + if not matches: + raise ValueError(f"No account found matching '{name}'") + if len(matches) > 1: + names = [acc["entity"].get("name", "") for acc in matches] + raise ValueError(f"Multiple accounts match '{name}': {names}") + + return Account(client=self, id_cache=matches[0]["entity"]["_id"]) + def get_default_organization(self) -> Optional[Account]: accounts = self._fetch_user_accounts() organizations = [acc for acc in accounts if acc["entity"].get("type") == "organization"] - + if not organizations: return None - + for org in organizations: if org.get("isDefault"): return Account(client=self, id_cache=org["entity"]["_id"]) - + return Account(client=self, id_cache=organizations[0]["entity"]["_id"]) From e6a425b4021027945080d56a819b5ff7961713f4 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 21 Jan 2026 18:22:47 -0800 Subject: [PATCH 04/12] update: add test for client --- tests/py/unit/test_client.py | 188 +++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/tests/py/unit/test_client.py b/tests/py/unit/test_client.py index 59306ad..f3496cc 100644 --- a/tests/py/unit/test_client.py +++ b/tests/py/unit/test_client.py @@ -17,6 +17,28 @@ ME_ACCOUNT_ID = "my-account-id" USERS_ME_RESPONSE = {"data": {"user": {"entity": {"defaultAccountId": ME_ACCOUNT_ID}}}} +ACCOUNTS_RESPONSE = { + "data": { + "user": { + "entity": {"defaultAccountId": ME_ACCOUNT_ID}, + "accounts": [ + { + "entity": {"_id": "user-acc-1", "name": "John Doe", "type": "user"}, + "isDefault": True, + }, + { + "entity": {"_id": "org-acc-1", "name": "Acme Corp", "type": "organization"}, + "isDefault": True, + }, + { + "entity": {"_id": "org-acc-2", "name": "Beta Industries", "type": "organization"}, + "isDefault": False, + }, + ], + } + } +} + class APIClientUnitTest(EndpointBaseUnitTest): def _base_env(self): @@ -71,3 +93,169 @@ def test_my_account_id_fetches_and_caches(self, mock_get): self.assertEqual(mock_get.call_args[1]["headers"]["Authorization"], f"Bearer {OIDC_ACCESS_TOKEN}") self.assertEqual(mock_get.call_args[1]["timeout"], 30) self.assertEqual(os.environ.get("ACCOUNT_ID"), ME_ACCOUNT_ID) + + @mock.patch("requests.get") + def test_list_accounts(self, mock_get): + env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} + with mock.patch.dict("os.environ", env, clear=True): + mock_resp = mock.Mock() + mock_resp.json.return_value = ACCOUNTS_RESPONSE + mock_resp.raise_for_status.return_value = None + mock_get.return_value = mock_resp + + client = APIClient.authenticate() + accounts = client.list_accounts() + + self.assertEqual(len(accounts), 3) + self.assertEqual(accounts[0]["id"], "user-acc-1") + self.assertEqual(accounts[0]["name"], "John Doe") + self.assertEqual(accounts[0]["type"], "user") + self.assertTrue(accounts[0]["isDefault"]) + self.assertEqual(accounts[1]["id"], "org-acc-1") + self.assertEqual(accounts[1]["name"], "Acme Corp") + self.assertEqual(accounts[1]["type"], "organization") + + @mock.patch("requests.get") + def test_get_account_by_index(self, mock_get): + env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} + with mock.patch.dict("os.environ", env, clear=True): + mock_resp = mock.Mock() + mock_resp.json.return_value = ACCOUNTS_RESPONSE + mock_resp.raise_for_status.return_value = None + mock_get.return_value = mock_resp + + client = APIClient.authenticate() + account = client.get_account(index=1) + self.assertEqual(account.id_cache, "org-acc-1") + + @mock.patch("requests.get") + def test_get_account_by_name(self, mock_get): + env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} + with mock.patch.dict("os.environ", env, clear=True): + mock_resp = mock.Mock() + mock_resp.json.return_value = ACCOUNTS_RESPONSE + mock_resp.raise_for_status.return_value = None + mock_get.return_value = mock_resp + + client = APIClient.authenticate() + account = client.get_account(name="Acme") + self.assertEqual(account.id_cache, "org-acc-1") + + @mock.patch("requests.get") + def test_get_account_by_name_regex(self, mock_get): + env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} + with mock.patch.dict("os.environ", env, clear=True): + mock_resp = mock.Mock() + mock_resp.json.return_value = ACCOUNTS_RESPONSE + mock_resp.raise_for_status.return_value = None + mock_get.return_value = mock_resp + + client = APIClient.authenticate() + account = client.get_account(name="Beta.*") + self.assertEqual(account.id_cache, "org-acc-2") + + @mock.patch("requests.get") + def test_get_account_no_params_raises(self, mock_get): + env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} + with mock.patch.dict("os.environ", env, clear=True): + client = APIClient.authenticate() + with self.assertRaises(ValueError) as ctx: + client.get_account() + self.assertIn("Either 'name' or 'index' must be provided", str(ctx.exception)) + + @mock.patch("requests.get") + def test_get_account_no_match_raises(self, mock_get): + env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} + with mock.patch.dict("os.environ", env, clear=True): + mock_resp = mock.Mock() + mock_resp.json.return_value = ACCOUNTS_RESPONSE + mock_resp.raise_for_status.return_value = None + mock_get.return_value = mock_resp + + client = APIClient.authenticate() + with self.assertRaises(ValueError) as ctx: + client.get_account(name="NonExistent") + self.assertIn("No account found matching 'NonExistent'", str(ctx.exception)) + + @mock.patch("requests.get") + def test_get_account_multiple_matches_raises(self, mock_get): + env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} + with mock.patch.dict("os.environ", env, clear=True): + mock_resp = mock.Mock() + mock_resp.json.return_value = ACCOUNTS_RESPONSE + mock_resp.raise_for_status.return_value = None + mock_get.return_value = mock_resp + + client = APIClient.authenticate() + with self.assertRaises(ValueError) as ctx: + client.get_account(name=".*") + self.assertIn("Multiple accounts match", str(ctx.exception)) + + @mock.patch("requests.get") + def test_my_organization_returns_default(self, mock_get): + env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} + with mock.patch.dict("os.environ", env, clear=True): + mock_resp = mock.Mock() + mock_resp.json.return_value = ACCOUNTS_RESPONSE + mock_resp.raise_for_status.return_value = None + mock_get.return_value = mock_resp + + client = APIClient.authenticate() + org = client.my_organization + self.assertEqual(org.id_cache, "org-acc-1") + + @mock.patch("requests.get") + def test_my_organization_returns_first_if_no_default(self, mock_get): + env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} + with mock.patch.dict("os.environ", env, clear=True): + response_no_default = { + "data": { + "user": { + "entity": {"defaultAccountId": ME_ACCOUNT_ID}, + "accounts": [ + { + "entity": {"_id": "org-acc-1", "name": "Org 1", "type": "organization"}, + "isDefault": False, + }, + { + "entity": {"_id": "org-acc-2", "name": "Org 2", "type": "organization"}, + "isDefault": False, + }, + ], + } + } + } + mock_resp = mock.Mock() + mock_resp.json.return_value = response_no_default + mock_resp.raise_for_status.return_value = None + mock_get.return_value = mock_resp + + client = APIClient.authenticate() + org = client.my_organization + self.assertEqual(org.id_cache, "org-acc-1") + + @mock.patch("requests.get") + def test_my_organization_returns_none_if_no_orgs(self, mock_get): + env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} + with mock.patch.dict("os.environ", env, clear=True): + response_no_orgs = { + "data": { + "user": { + "entity": {"defaultAccountId": ME_ACCOUNT_ID}, + "accounts": [ + { + "entity": {"_id": "user-acc-1", "name": "User", "type": "user"}, + "isDefault": True, + }, + ], + } + } + } + mock_resp = mock.Mock() + mock_resp.json.return_value = response_no_orgs + mock_resp.raise_for_status.return_value = None + mock_get.return_value = mock_resp + + client = APIClient.authenticate() + org = client.my_organization + self.assertIsNone(org) From c12ae6dedc81d3587d7dae3f147b99e5feb4a578 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 21 Jan 2026 18:29:04 -0800 Subject: [PATCH 05/12] update: cleanup test --- tests/py/unit/test_client.py | 151 +++-------------------------------- 1 file changed, 11 insertions(+), 140 deletions(-) diff --git a/tests/py/unit/test_client.py b/tests/py/unit/test_client.py index f3496cc..e155002 100644 --- a/tests/py/unit/test_client.py +++ b/tests/py/unit/test_client.py @@ -49,9 +49,9 @@ def _base_env(self): "API_SECURE": API_SECURE_FALSE, } - def _mock_users_me(self, mock_get): + def _mock_users_me(self, mock_get, response=None): mock_resp = mock.Mock() - mock_resp.json.return_value = USERS_ME_RESPONSE + mock_resp.json.return_value = response or USERS_ME_RESPONSE mock_resp.raise_for_status.return_value = None mock_get.return_value = mock_resp @@ -98,11 +98,7 @@ def test_my_account_id_fetches_and_caches(self, mock_get): def test_list_accounts(self, mock_get): env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} with mock.patch.dict("os.environ", env, clear=True): - mock_resp = mock.Mock() - mock_resp.json.return_value = ACCOUNTS_RESPONSE - mock_resp.raise_for_status.return_value = None - mock_get.return_value = mock_resp - + self._mock_users_me(mock_get, ACCOUNTS_RESPONSE) client = APIClient.authenticate() accounts = client.list_accounts() @@ -116,146 +112,21 @@ def test_list_accounts(self, mock_get): self.assertEqual(accounts[1]["type"], "organization") @mock.patch("requests.get") - def test_get_account_by_index(self, mock_get): - env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} - with mock.patch.dict("os.environ", env, clear=True): - mock_resp = mock.Mock() - mock_resp.json.return_value = ACCOUNTS_RESPONSE - mock_resp.raise_for_status.return_value = None - mock_get.return_value = mock_resp - - client = APIClient.authenticate() - account = client.get_account(index=1) - self.assertEqual(account.id_cache, "org-acc-1") - - @mock.patch("requests.get") - def test_get_account_by_name(self, mock_get): - env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} - with mock.patch.dict("os.environ", env, clear=True): - mock_resp = mock.Mock() - mock_resp.json.return_value = ACCOUNTS_RESPONSE - mock_resp.raise_for_status.return_value = None - mock_get.return_value = mock_resp - - client = APIClient.authenticate() - account = client.get_account(name="Acme") - self.assertEqual(account.id_cache, "org-acc-1") - - @mock.patch("requests.get") - def test_get_account_by_name_regex(self, mock_get): - env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} - with mock.patch.dict("os.environ", env, clear=True): - mock_resp = mock.Mock() - mock_resp.json.return_value = ACCOUNTS_RESPONSE - mock_resp.raise_for_status.return_value = None - mock_get.return_value = mock_resp - - client = APIClient.authenticate() - account = client.get_account(name="Beta.*") - self.assertEqual(account.id_cache, "org-acc-2") - - @mock.patch("requests.get") - def test_get_account_no_params_raises(self, mock_get): + def test_get_account(self, mock_get): env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} with mock.patch.dict("os.environ", env, clear=True): + self._mock_users_me(mock_get, ACCOUNTS_RESPONSE) client = APIClient.authenticate() - with self.assertRaises(ValueError) as ctx: - client.get_account() - self.assertIn("Either 'name' or 'index' must be provided", str(ctx.exception)) + + self.assertEqual(client.get_account(index=1).id_cache, "org-acc-1") + self.assertEqual(client.get_account(name="Acme").id_cache, "org-acc-1") + self.assertEqual(client.get_account(name="Beta.*").id_cache, "org-acc-2") @mock.patch("requests.get") - def test_get_account_no_match_raises(self, mock_get): + def test_my_organization(self, mock_get): env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} with mock.patch.dict("os.environ", env, clear=True): - mock_resp = mock.Mock() - mock_resp.json.return_value = ACCOUNTS_RESPONSE - mock_resp.raise_for_status.return_value = None - mock_get.return_value = mock_resp - - client = APIClient.authenticate() - with self.assertRaises(ValueError) as ctx: - client.get_account(name="NonExistent") - self.assertIn("No account found matching 'NonExistent'", str(ctx.exception)) - - @mock.patch("requests.get") - def test_get_account_multiple_matches_raises(self, mock_get): - env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} - with mock.patch.dict("os.environ", env, clear=True): - mock_resp = mock.Mock() - mock_resp.json.return_value = ACCOUNTS_RESPONSE - mock_resp.raise_for_status.return_value = None - mock_get.return_value = mock_resp - - client = APIClient.authenticate() - with self.assertRaises(ValueError) as ctx: - client.get_account(name=".*") - self.assertIn("Multiple accounts match", str(ctx.exception)) - - @mock.patch("requests.get") - def test_my_organization_returns_default(self, mock_get): - env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} - with mock.patch.dict("os.environ", env, clear=True): - mock_resp = mock.Mock() - mock_resp.json.return_value = ACCOUNTS_RESPONSE - mock_resp.raise_for_status.return_value = None - mock_get.return_value = mock_resp - + self._mock_users_me(mock_get, ACCOUNTS_RESPONSE) client = APIClient.authenticate() org = client.my_organization self.assertEqual(org.id_cache, "org-acc-1") - - @mock.patch("requests.get") - def test_my_organization_returns_first_if_no_default(self, mock_get): - env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} - with mock.patch.dict("os.environ", env, clear=True): - response_no_default = { - "data": { - "user": { - "entity": {"defaultAccountId": ME_ACCOUNT_ID}, - "accounts": [ - { - "entity": {"_id": "org-acc-1", "name": "Org 1", "type": "organization"}, - "isDefault": False, - }, - { - "entity": {"_id": "org-acc-2", "name": "Org 2", "type": "organization"}, - "isDefault": False, - }, - ], - } - } - } - mock_resp = mock.Mock() - mock_resp.json.return_value = response_no_default - mock_resp.raise_for_status.return_value = None - mock_get.return_value = mock_resp - - client = APIClient.authenticate() - org = client.my_organization - self.assertEqual(org.id_cache, "org-acc-1") - - @mock.patch("requests.get") - def test_my_organization_returns_none_if_no_orgs(self, mock_get): - env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} - with mock.patch.dict("os.environ", env, clear=True): - response_no_orgs = { - "data": { - "user": { - "entity": {"defaultAccountId": ME_ACCOUNT_ID}, - "accounts": [ - { - "entity": {"_id": "user-acc-1", "name": "User", "type": "user"}, - "isDefault": True, - }, - ], - } - } - } - mock_resp = mock.Mock() - mock_resp.json.return_value = response_no_orgs - mock_resp.raise_for_status.return_value = None - mock_get.return_value = mock_resp - - client = APIClient.authenticate() - org = client.my_organization - self.assertIsNone(org) From 0c0f00a9896d075d109bb4c906d3dc4fd79f2ff2 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 21 Jan 2026 18:41:12 -0800 Subject: [PATCH 06/12] chore: cleanup url creation --- src/py/mat3ra/api_client/client.py | 16 ++++++++-------- src/py/mat3ra/api_client/models.py | 12 ++++-------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/py/mat3ra/api_client/client.py b/src/py/mat3ra/api_client/client.py index 0ba6f37..dc8dc82 100644 --- a/src/py/mat3ra/api_client/client.py +++ b/src/py/mat3ra/api_client/client.py @@ -5,7 +5,7 @@ import requests from pydantic import BaseModel, ConfigDict -from .constants import ACCESS_TOKEN_ENV_VAR +from .constants import ACCESS_TOKEN_ENV_VAR, _build_base_url from .endpoints.bank_materials import BankMaterialEndpoints from .endpoints.bank_workflows import BankWorkflowEndpoints from .endpoints.jobs import JobEndpoints @@ -140,18 +140,18 @@ def authenticate( return cls(host=host_value, port=port_value, version=version_value, secure=secure_value, auth=auth, timeout_seconds=timeout_seconds) - def _fetch_user_accounts(self) -> List[dict]: + def _fetch_user_data(self) -> dict: access_token = self.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR) if not access_token: - raise ValueError("Access token is required to fetch accounts") - - protocol = "https" if self.secure else "http" - port_str = f":{self.port}" if self.port not in (80, 443) else "" - url = f"{protocol}://{self.host}{port_str}/api/v1/users/me" + raise ValueError("Access token is required to fetch user data") + url = _build_base_url(self.host, self.port, self.secure, "/api/v1/users/me") response = requests.get(url, headers={"Authorization": f"Bearer {access_token}"}, timeout=30) response.raise_for_status() - return response.json()["data"]["user"].get("accounts", []) + return response.json()["data"]["user"] + + def _fetch_user_accounts(self) -> List[dict]: + return self._fetch_user_data().get("accounts", []) def list_accounts(self) -> List[dict]: accounts = self._fetch_user_accounts() diff --git a/src/py/mat3ra/api_client/models.py b/src/py/mat3ra/api_client/models.py index 290c6c9..a4c00d3 100644 --- a/src/py/mat3ra/api_client/models.py +++ b/src/py/mat3ra/api_client/models.py @@ -1,10 +1,9 @@ import os from typing import Any, Optional -import requests from pydantic import BaseModel, ConfigDict, Field -from .constants import ACCESS_TOKEN_ENV_VAR, ACCOUNT_ID_ENV_VAR, AUTH_TOKEN_ENV_VAR, _build_base_url +from .constants import ACCESS_TOKEN_ENV_VAR, ACCOUNT_ID_ENV_VAR, AUTH_TOKEN_ENV_VAR class AuthContext(BaseModel): @@ -53,14 +52,11 @@ def _resolve_account_id(self) -> str: self.client.auth.account_id = account_id return account_id - access_token = self.client.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR) - if not access_token: + if not (self.client.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR)): raise ValueError("ACCOUNT_ID is not set and no OIDC access token is available.") - url = _build_base_url(self.client.host, self.client.port, self.client.secure, "/api/v1/users/me") - response = requests.get(url, headers={"Authorization": f"Bearer {access_token}"}, timeout=30) - response.raise_for_status() - account_id = response.json()["data"]["user"]["entity"]["defaultAccountId"] + user_data = self.client._fetch_user_data() + account_id = user_data["entity"]["defaultAccountId"] os.environ[ACCOUNT_ID_ENV_VAR] = account_id self.client.auth.account_id = account_id return account_id From 9a320501c32b25d4169134ed2335cba8b757a1bc Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 21 Jan 2026 19:10:32 -0800 Subject: [PATCH 07/12] update: cleanup --- src/py/mat3ra/api_client/client.py | 66 +++++++++++------------------- 1 file changed, 24 insertions(+), 42 deletions(-) diff --git a/src/py/mat3ra/api_client/client.py b/src/py/mat3ra/api_client/client.py index dc8dc82..26e50c4 100644 --- a/src/py/mat3ra/api_client/client.py +++ b/src/py/mat3ra/api_client/client.py @@ -48,39 +48,17 @@ def auth_env(cls) -> AuthEnv: return AuthEnv.from_env() def _init_endpoints(self, timeout_seconds: int) -> None: - kwargs = {"timeout": timeout_seconds, "auth": self.auth} - account_id = self.auth.account_id or "" - auth_token = self.auth.auth_token or "" - self._init_core_endpoints(kwargs, account_id, auth_token) - self._init_bank_endpoints(kwargs, account_id, auth_token) - - def _init_core_endpoints(self, kwargs: dict, account_id: str, auth_token: str) -> None: - self.materials = MaterialEndpoints( - self.host, self.port, account_id, auth_token, version=self.version, secure=self.secure, **kwargs - ) - self.workflows = WorkflowEndpoints( - self.host, self.port, account_id, auth_token, version=self.version, secure=self.secure, **kwargs - ) - self.jobs = JobEndpoints( - self.host, self.port, account_id, auth_token, version=self.version, secure=self.secure, **kwargs - ) - self.projects = ProjectEndpoints( - self.host, self.port, account_id, auth_token, version=self.version, secure=self.secure, **kwargs - ) - self.properties = PropertiesEndpoints( - self.host, self.port, account_id, auth_token, version=self.version, secure=self.secure, **kwargs - ) - self.metaproperties = MetaPropertiesEndpoints( - self.host, self.port, account_id, auth_token, version=self.version, secure=self.secure, **kwargs - ) - - def _init_bank_endpoints(self, kwargs: dict, account_id: str, auth_token: str) -> None: - self.bank_materials = BankMaterialEndpoints( - self.host, self.port, account_id, auth_token, version=self.version, secure=self.secure, **kwargs - ) - self.bank_workflows = BankWorkflowEndpoints( - self.host, self.port, account_id, auth_token, version=self.version, secure=self.secure, **kwargs - ) + base_args = (self.host, self.port, self.auth.account_id or "", self.auth.auth_token or "") + base_kwargs = {"version": self.version, "secure": self.secure, "timeout": timeout_seconds, "auth": self.auth} + + self.materials = MaterialEndpoints(*base_args, **base_kwargs) + self.workflows = WorkflowEndpoints(*base_args, **base_kwargs) + self.jobs = JobEndpoints(*base_args, **base_kwargs) + self.projects = ProjectEndpoints(*base_args, **base_kwargs) + self.properties = PropertiesEndpoints(*base_args, **base_kwargs) + self.metaproperties = MetaPropertiesEndpoints(*base_args, **base_kwargs) + self.bank_materials = BankMaterialEndpoints(*base_args, **base_kwargs) + self.bank_workflows = BankWorkflowEndpoints(*base_args, **base_kwargs) @staticmethod def _resolve_config( @@ -133,12 +111,19 @@ def authenticate( auth_token: Optional[str] = None, timeout_seconds: int = 60, ) -> "APIClient": - host_value, port_value, version_value, secure_value = cls._resolve_config(host, port, version, secure, - cls.env()) + host_value, port_value, version_value, secure_value = cls._resolve_config( + host, port, version, secure, cls.env() + ) auth = cls._auth_from_env(access_token=access_token, account_id=account_id, auth_token=auth_token) cls._validate_auth(auth) - return cls(host=host_value, port=port_value, version=version_value, secure=secure_value, auth=auth, - timeout_seconds=timeout_seconds) + return cls( + host=host_value, + port=port_value, + version=version_value, + secure=secure_value, + auth=auth, + timeout_seconds=timeout_seconds, + ) def _fetch_user_data(self) -> dict: access_token = self.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR) @@ -193,8 +178,5 @@ def get_default_organization(self) -> Optional[Account]: if not organizations: return None - for org in organizations: - if org.get("isDefault"): - return Account(client=self, id_cache=org["entity"]["_id"]) - - return Account(client=self, id_cache=organizations[0]["entity"]["_id"]) + default_org = next((org for org in organizations if org.get("isDefault")), organizations[0]) + return Account(client=self, id_cache=default_org["entity"]["_id"]) From ddb46fae50e9533e394cb17d313e752ab9dc9b6a Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 22 Jan 2026 20:04:10 -0800 Subject: [PATCH 08/12] update: fecth data correctly --- src/py/mat3ra/api_client/client.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/api_client/client.py b/src/py/mat3ra/api_client/client.py index 26e50c4..fa2453a 100644 --- a/src/py/mat3ra/api_client/client.py +++ b/src/py/mat3ra/api_client/client.py @@ -50,7 +50,7 @@ def auth_env(cls) -> AuthEnv: def _init_endpoints(self, timeout_seconds: int) -> None: base_args = (self.host, self.port, self.auth.account_id or "", self.auth.auth_token or "") base_kwargs = {"version": self.version, "secure": self.secure, "timeout": timeout_seconds, "auth": self.auth} - + self.materials = MaterialEndpoints(*base_args, **base_kwargs) self.workflows = WorkflowEndpoints(*base_args, **base_kwargs) self.jobs = JobEndpoints(*base_args, **base_kwargs) @@ -125,7 +125,7 @@ def authenticate( timeout_seconds=timeout_seconds, ) - def _fetch_user_data(self) -> dict: + def _fetch_data(self) -> dict: access_token = self.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR) if not access_token: raise ValueError("Access token is required to fetch user data") @@ -133,10 +133,11 @@ def _fetch_user_data(self) -> dict: url = _build_base_url(self.host, self.port, self.secure, "/api/v1/users/me") response = requests.get(url, headers={"Authorization": f"Bearer {access_token}"}, timeout=30) response.raise_for_status() - return response.json()["data"]["user"] + + return response.json()["data"] def _fetch_user_accounts(self) -> List[dict]: - return self._fetch_user_data().get("accounts", []) + return self._fetch_data().get("accounts", []) def list_accounts(self) -> List[dict]: accounts = self._fetch_user_accounts() From 3e0e2c2dda9fc8a98eaf7179938691b5f9ab4c43 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 22 Jan 2026 20:06:14 -0800 Subject: [PATCH 09/12] update: adjustments --- src/py/mat3ra/api_client/client.py | 21 ++++++++++----------- tests/py/unit/test_client.py | 8 ++++---- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/py/mat3ra/api_client/client.py b/src/py/mat3ra/api_client/client.py index fa2453a..3f6f8cd 100644 --- a/src/py/mat3ra/api_client/client.py +++ b/src/py/mat3ra/api_client/client.py @@ -125,7 +125,7 @@ def authenticate( timeout_seconds=timeout_seconds, ) - def _fetch_data(self) -> dict: + def _fetch_user_data(self) -> dict: access_token = self.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR) if not access_token: raise ValueError("Access token is required to fetch user data") @@ -133,22 +133,21 @@ def _fetch_data(self) -> dict: url = _build_base_url(self.host, self.port, self.secure, "/api/v1/users/me") response = requests.get(url, headers={"Authorization": f"Bearer {access_token}"}, timeout=30) response.raise_for_status() - - return response.json()["data"] + return response.json()["data"]["user"] def _fetch_user_accounts(self) -> List[dict]: - return self._fetch_data().get("accounts", []) + return self._fetch_user_data().get("accounts", []) def list_accounts(self) -> List[dict]: accounts = self._fetch_user_accounts() return [ { - "id": acc["entity"]["_id"], - "name": acc["entity"].get("name", ""), - "type": acc["entity"].get("type", "user"), - "isDefault": acc.get("isDefault", False), + "id": account["entity"]["_id"], + "name": account["entity"].get("name", ""), + "type": account["entity"].get("type", "personal"), + "isDefault": account.get("isDefault", False), } - for acc in accounts + for account in accounts ] def get_account(self, name: Optional[str] = None, index: Optional[int] = None) -> Account: @@ -162,7 +161,7 @@ def get_account(self, name: Optional[str] = None, index: Optional[int] = None) - return Account(client=self, id_cache=accounts[index]["entity"]["_id"]) pattern = re.compile(name, re.IGNORECASE) - matches = [acc for acc in accounts if pattern.search(acc["entity"].get("name", ""))] + matches = [account for account in accounts if pattern.search(account["entity"].get("name", ""))] if not matches: raise ValueError(f"No account found matching '{name}'") @@ -174,7 +173,7 @@ def get_account(self, name: Optional[str] = None, index: Optional[int] = None) - def get_default_organization(self) -> Optional[Account]: accounts = self._fetch_user_accounts() - organizations = [acc for acc in accounts if acc["entity"].get("type") == "organization"] + organizations = [account for account in accounts if account["entity"].get("type") in ("organization", "enterprise")] if not organizations: return None diff --git a/tests/py/unit/test_client.py b/tests/py/unit/test_client.py index e155002..c1bf7e1 100644 --- a/tests/py/unit/test_client.py +++ b/tests/py/unit/test_client.py @@ -23,11 +23,11 @@ "entity": {"defaultAccountId": ME_ACCOUNT_ID}, "accounts": [ { - "entity": {"_id": "user-acc-1", "name": "John Doe", "type": "user"}, + "entity": {"_id": "user-acc-1", "name": "John Doe", "type": "personal"}, "isDefault": True, }, { - "entity": {"_id": "org-acc-1", "name": "Acme Corp", "type": "organization"}, + "entity": {"_id": "org-acc-1", "name": "Acme Corp", "type": "enterprise"}, "isDefault": True, }, { @@ -105,11 +105,11 @@ def test_list_accounts(self, mock_get): self.assertEqual(len(accounts), 3) self.assertEqual(accounts[0]["id"], "user-acc-1") self.assertEqual(accounts[0]["name"], "John Doe") - self.assertEqual(accounts[0]["type"], "user") + self.assertEqual(accounts[0]["type"], "personal") self.assertTrue(accounts[0]["isDefault"]) self.assertEqual(accounts[1]["id"], "org-acc-1") self.assertEqual(accounts[1]["name"], "Acme Corp") - self.assertEqual(accounts[1]["type"], "organization") + self.assertEqual(accounts[1]["type"], "enterprise") @mock.patch("requests.get") def test_get_account(self, mock_get): From 6f18170829ba5f838c1713ceecf5107410fae372 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 22 Jan 2026 20:09:54 -0800 Subject: [PATCH 10/12] update: adjustments --- src/py/mat3ra/api_client/client.py | 6 +++--- src/py/mat3ra/api_client/models.py | 4 ++-- tests/py/unit/test_client.py | 32 +++++++++++++++--------------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/py/mat3ra/api_client/client.py b/src/py/mat3ra/api_client/client.py index 3f6f8cd..3d11fbc 100644 --- a/src/py/mat3ra/api_client/client.py +++ b/src/py/mat3ra/api_client/client.py @@ -125,7 +125,7 @@ def authenticate( timeout_seconds=timeout_seconds, ) - def _fetch_user_data(self) -> dict: + def _fetch_data(self) -> dict: access_token = self.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR) if not access_token: raise ValueError("Access token is required to fetch user data") @@ -133,10 +133,10 @@ def _fetch_user_data(self) -> dict: url = _build_base_url(self.host, self.port, self.secure, "/api/v1/users/me") response = requests.get(url, headers={"Authorization": f"Bearer {access_token}"}, timeout=30) response.raise_for_status() - return response.json()["data"]["user"] + return response.json()["data"] def _fetch_user_accounts(self) -> List[dict]: - return self._fetch_user_data().get("accounts", []) + return self._fetch_data().get("accounts", []) def list_accounts(self) -> List[dict]: accounts = self._fetch_user_accounts() diff --git a/src/py/mat3ra/api_client/models.py b/src/py/mat3ra/api_client/models.py index a4c00d3..4d1cc5b 100644 --- a/src/py/mat3ra/api_client/models.py +++ b/src/py/mat3ra/api_client/models.py @@ -55,8 +55,8 @@ def _resolve_account_id(self) -> str: if not (self.client.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR)): raise ValueError("ACCOUNT_ID is not set and no OIDC access token is available.") - user_data = self.client._fetch_user_data() - account_id = user_data["entity"]["defaultAccountId"] + data = self.client._fetch_data() + account_id = data["user"]["entity"]["defaultAccountId"] os.environ[ACCOUNT_ID_ENV_VAR] = account_id self.client.auth.account_id = account_id return account_id diff --git a/tests/py/unit/test_client.py b/tests/py/unit/test_client.py index c1bf7e1..e1ee43a 100644 --- a/tests/py/unit/test_client.py +++ b/tests/py/unit/test_client.py @@ -20,22 +20,22 @@ ACCOUNTS_RESPONSE = { "data": { "user": { - "entity": {"defaultAccountId": ME_ACCOUNT_ID}, - "accounts": [ - { - "entity": {"_id": "user-acc-1", "name": "John Doe", "type": "personal"}, - "isDefault": True, - }, - { - "entity": {"_id": "org-acc-1", "name": "Acme Corp", "type": "enterprise"}, - "isDefault": True, - }, - { - "entity": {"_id": "org-acc-2", "name": "Beta Industries", "type": "organization"}, - "isDefault": False, - }, - ], - } + "entity": {"defaultAccountId": ME_ACCOUNT_ID} + }, + "accounts": [ + { + "entity": {"_id": "user-acc-1", "name": "John Doe", "type": "personal"}, + "isDefault": True, + }, + { + "entity": {"_id": "org-acc-1", "name": "Acme Corp", "type": "enterprise"}, + "isDefault": True, + }, + { + "entity": {"_id": "org-acc-2", "name": "Beta Industries", "type": "organization"}, + "isDefault": False, + }, + ], } } From fc324626794ccf1c4472c77defd9f4db24636160 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 22 Jan 2026 21:02:14 -0800 Subject: [PATCH 11/12] update: add name property --- src/py/mat3ra/api_client/client.py | 6 ++--- src/py/mat3ra/api_client/models.py | 42 ++++++++++++++++++++++-------- tests/py/unit/test_client.py | 22 ++++++++++++---- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/src/py/mat3ra/api_client/client.py b/src/py/mat3ra/api_client/client.py index 3d11fbc..8193995 100644 --- a/src/py/mat3ra/api_client/client.py +++ b/src/py/mat3ra/api_client/client.py @@ -158,7 +158,7 @@ def get_account(self, name: Optional[str] = None, index: Optional[int] = None) - accounts = self._fetch_user_accounts() if index is not None: - return Account(client=self, id_cache=accounts[index]["entity"]["_id"]) + return Account(client=self, entity_cache=accounts[index]["entity"]) pattern = re.compile(name, re.IGNORECASE) matches = [account for account in accounts if pattern.search(account["entity"].get("name", ""))] @@ -169,7 +169,7 @@ def get_account(self, name: Optional[str] = None, index: Optional[int] = None) - names = [acc["entity"].get("name", "") for acc in matches] raise ValueError(f"Multiple accounts match '{name}': {names}") - return Account(client=self, id_cache=matches[0]["entity"]["_id"]) + return Account(client=self, entity_cache=matches[0]["entity"]) def get_default_organization(self) -> Optional[Account]: accounts = self._fetch_user_accounts() @@ -179,4 +179,4 @@ def get_default_organization(self) -> Optional[Account]: return None default_org = next((org for org in organizations if org.get("isDefault")), organizations[0]) - return Account(client=self, id_cache=default_org["entity"]["_id"]) + return Account(client=self, entity_cache=default_org["entity"]) diff --git a/src/py/mat3ra/api_client/models.py b/src/py/mat3ra/api_client/models.py index 4d1cc5b..e834ed8 100644 --- a/src/py/mat3ra/api_client/models.py +++ b/src/py/mat3ra/api_client/models.py @@ -37,27 +37,47 @@ class Account(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) client: Any = Field(exclude=True, repr=False) - id_cache: Optional[str] = None + entity_cache: Optional[dict] = None @property def id(self) -> str: - if self.id_cache: - return self.id_cache - self.id_cache = self._resolve_account_id() - return self.id_cache + if not self.entity_cache: + self._get_entity() + return self.entity_cache["_id"] - def _resolve_account_id(self) -> str: + @property + def name(self) -> str: + if not self.entity_cache: + self._get_entity() + return self.entity_cache.get("name", "") + + def _get_entity(self) -> None: + account_id, accounts = self._get_account_id_and_accounts() + self.entity_cache = self._find_account_entity(account_id, accounts) + + def _get_account_id_and_accounts(self) -> tuple[str, Optional[list]]: account_id = self.client.auth.account_id or os.environ.get(ACCOUNT_ID_ENV_VAR) + if account_id: - self.client.auth.account_id = account_id - return account_id - + return account_id, None + if not (self.client.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR)): raise ValueError("ACCOUNT_ID is not set and no OIDC access token is available.") - + data = self.client._fetch_data() account_id = data["user"]["entity"]["defaultAccountId"] os.environ[ACCOUNT_ID_ENV_VAR] = account_id self.client.auth.account_id = account_id - return account_id + return account_id, data.get("accounts", []) + + def _find_account_entity(self, account_id: str, accounts: Optional[list]) -> dict: + if accounts is None and (self.client.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR)): + accounts = self.client._fetch_user_accounts() + + if accounts: + for account in accounts: + if account["entity"]["_id"] == account_id: + return account["entity"] + + return {"_id": account_id} diff --git a/tests/py/unit/test_client.py b/tests/py/unit/test_client.py index e1ee43a..6be476b 100644 --- a/tests/py/unit/test_client.py +++ b/tests/py/unit/test_client.py @@ -83,8 +83,16 @@ def test_my_account_id_uses_existing_account_id(self, mock_get): @mock.patch("requests.get") def test_my_account_id_fetches_and_caches(self, mock_get): env = self._base_env() | {"OIDC_ACCESS_TOKEN": OIDC_ACCESS_TOKEN} + response_with_account = { + "data": { + "user": {"entity": {"defaultAccountId": ME_ACCOUNT_ID}}, + "accounts": [ + {"entity": {"_id": ME_ACCOUNT_ID, "name": "Test User", "type": "personal"}, "isDefault": True} + ], + } + } with mock.patch.dict("os.environ", env, clear=True): - self._mock_users_me(mock_get) + self._mock_users_me(mock_get, response_with_account) client = APIClient.authenticate() self.assertEqual(client.my_account.id, ME_ACCOUNT_ID) self.assertEqual(client.my_account.id, ME_ACCOUNT_ID) @@ -118,9 +126,12 @@ def test_get_account(self, mock_get): self._mock_users_me(mock_get, ACCOUNTS_RESPONSE) client = APIClient.authenticate() - self.assertEqual(client.get_account(index=1).id_cache, "org-acc-1") - self.assertEqual(client.get_account(name="Acme").id_cache, "org-acc-1") - self.assertEqual(client.get_account(name="Beta.*").id_cache, "org-acc-2") + account = client.get_account(index=1) + self.assertEqual(account.id, "org-acc-1") + self.assertEqual(account.name, "Acme Corp") + + self.assertEqual(client.get_account(name="Acme").id, "org-acc-1") + self.assertEqual(client.get_account(name="Beta.*").id, "org-acc-2") @mock.patch("requests.get") def test_my_organization(self, mock_get): @@ -129,4 +140,5 @@ def test_my_organization(self, mock_get): self._mock_users_me(mock_get, ACCOUNTS_RESPONSE) client = APIClient.authenticate() org = client.my_organization - self.assertEqual(org.id_cache, "org-acc-1") + self.assertEqual(org.id, "org-acc-1") + self.assertEqual(org.name, "Acme Corp") From b07cb35e2a0ca30b369255e0689401f402d51e13 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 22 Jan 2026 21:07:00 -0800 Subject: [PATCH 12/12] chore: lint fix --- src/py/mat3ra/api_client/client.py | 5 +++-- tests/py/unit/test_client.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/api_client/client.py b/src/py/mat3ra/api_client/client.py index 8193995..00914aa 100644 --- a/src/py/mat3ra/api_client/client.py +++ b/src/py/mat3ra/api_client/client.py @@ -142,7 +142,7 @@ def list_accounts(self) -> List[dict]: accounts = self._fetch_user_accounts() return [ { - "id": account["entity"]["_id"], + "_id": account["entity"]["_id"], "name": account["entity"].get("name", ""), "type": account["entity"].get("type", "personal"), "isDefault": account.get("isDefault", False), @@ -173,7 +173,8 @@ def get_account(self, name: Optional[str] = None, index: Optional[int] = None) - def get_default_organization(self) -> Optional[Account]: accounts = self._fetch_user_accounts() - organizations = [account for account in accounts if account["entity"].get("type") in ("organization", "enterprise")] + organizations = [account for account in accounts if + account["entity"].get("type") in ("organization", "enterprise")] if not organizations: return None diff --git a/tests/py/unit/test_client.py b/tests/py/unit/test_client.py index 6be476b..b19aa41 100644 --- a/tests/py/unit/test_client.py +++ b/tests/py/unit/test_client.py @@ -111,11 +111,11 @@ def test_list_accounts(self, mock_get): accounts = client.list_accounts() self.assertEqual(len(accounts), 3) - self.assertEqual(accounts[0]["id"], "user-acc-1") + self.assertEqual(accounts[0]["_id"], "user-acc-1") self.assertEqual(accounts[0]["name"], "John Doe") self.assertEqual(accounts[0]["type"], "personal") self.assertTrue(accounts[0]["isDefault"]) - self.assertEqual(accounts[1]["id"], "org-acc-1") + self.assertEqual(accounts[1]["_id"], "org-acc-1") self.assertEqual(accounts[1]["name"], "Acme Corp") self.assertEqual(accounts[1]["type"], "enterprise")