From 145974035dd426643c486315d2ad0607c2450e7d Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Wed, 5 Nov 2025 16:42:49 +0100 Subject: [PATCH 1/7] Add DesktopSessionExpired error type --- src/onepassword/errors.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/onepassword/errors.py b/src/onepassword/errors.py index df80c3a4..599e0757 100644 --- a/src/onepassword/errors.py +++ b/src/onepassword/errors.py @@ -3,6 +3,12 @@ import json +class DesktopSessionExpired(Exception): + def __init__(self, message): + self.message = message + super().__init__(self.message) + + class RateLimitExceededException(Exception): def __init__(self, message): self.message = message @@ -18,7 +24,9 @@ def raise_typed_exception(e: Exception): error_name = typed_error.get("name") message = typed_error.get("message") - if error_name == "RateLimitExceeded": + if error_name == "DesktopSessionExpired": + raise DesktopSessionExpired(message) + elif error_name == "RateLimitExceeded": raise RateLimitExceededException(message) elif message is not None: raise Exception(message) From ec9cdc2adde27f4353fd8cbe0bdbf04c96fba42d Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Thu, 6 Nov 2025 15:04:40 +0100 Subject: [PATCH 2/7] Properly raise typed exception for DesktopCore --- src/onepassword/desktop_core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/onepassword/desktop_core.py b/src/onepassword/desktop_core.py index c810585b..39d0135b 100644 --- a/src/onepassword/desktop_core.py +++ b/src/onepassword/desktop_core.py @@ -99,7 +99,9 @@ def call_shared_library(self, payload: str, operation_kind: str) -> bytes: success = parsed.get("success", False) if not success: - raise_typed_exception(Exception(str(payload))) + e = Exception() + e.msg = payload + raise_typed_exception(e) return payload From 5e8dde32e960dd44ad83a91f17c0e42c428dbd95 Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Thu, 6 Nov 2025 16:14:51 +0100 Subject: [PATCH 3/7] Add InnerClient class This class stores the client ID, the core used for making requests and the client config. This Inner client is capable of overriding its client id in case the desktop session expires due to the app getting locked. --- src/onepassword/core.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/onepassword/core.py b/src/onepassword/core.py index d47068a6..35d316e0 100644 --- a/src/onepassword/core.py +++ b/src/onepassword/core.py @@ -1,19 +1,44 @@ import json import platform from typing import Protocol -from onepassword.errors import raise_typed_exception +from onepassword.desktop_core import DesktopCore +from onepassword.errors import raise_typed_exception, DesktopSessionExpired # In empirical tests, we determined that maximum message size that can cross the FFI boundary # is ~128MB. Past this limit, FFI will throw an error and the program will crash. # We set the limit to 50MB to be safe and consistent with the other SDKs (where this limit is 64MB), to be reconsidered upon further testing MESSAGE_LIMIT = 50 * 1024 * 1024 + class Core(Protocol): async def init_client(self, client_config: dict) -> str: ... async def invoke(self, invoke_config: dict) -> str: ... def invoke_sync(self, invoke_config: dict) -> str: ... def release_client(self, client_id: int) -> None: ... + +class InnerClient: + client_id: int + core: DesktopCore | UniffiCore + config: dict[str, any] + + def __init__(self, client_id: int, core: "DesktopCore | UniffiCore", config: dict[str, any]): + self.client_id = client_id + self.core = core + self.config = config + + async def invoke(self, invoke_config: dict): + try: + return await self.core.invoke(invoke_config) + except DesktopSessionExpired as e: + new_client_id = await self.core.init_client(self.config) + self.client_id = new_client_id + invoke_config["invocation"]["clientId"] = self.client_id + return await self.core.invoke(invoke_config) + except Exception as e: + raise e + + class UniffiCore: def __init__(self): machine_arch = platform.machine().lower() From 33bd9ba8ffc5e624fa6b0875a8e322c5514c891e Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Thu, 6 Nov 2025 16:16:53 +0100 Subject: [PATCH 4/7] Use inner client --- src/onepassword/client.py | 12 +++++---- src/onepassword/groups.py | 11 ++++---- src/onepassword/items.py | 47 ++++++++++++++++----------------- src/onepassword/items_files.py | 23 ++++++++-------- src/onepassword/items_shares.py | 18 ++++++------- src/onepassword/secrets.py | 15 +++++------ src/onepassword/vaults.py | 31 +++++++++++----------- 7 files changed, 77 insertions(+), 80 deletions(-) diff --git a/src/onepassword/client.py b/src/onepassword/client.py index 0b3d319b..290cca96 100644 --- a/src/onepassword/client.py +++ b/src/onepassword/client.py @@ -2,7 +2,7 @@ from __future__ import annotations import weakref -from .core import UniffiCore +from .core import UniffiCore, InnerClient from .desktop_core import DesktopCore from .defaults import new_default_config, DesktopAuth from .secrets import Secrets @@ -34,12 +34,14 @@ async def authenticate( client_id = int(await core.init_client(config)) + inner_client = InnerClient(client_id, core, config) + authenticated_client = cls() - authenticated_client.secrets = Secrets(client_id, core) - authenticated_client.items = Items(client_id, core) - authenticated_client.vaults = Vaults(client_id, core) - authenticated_client.groups = Groups(client_id, core) + authenticated_client.secrets = Secrets(inner_client) + authenticated_client.items = Items(inner_client) + authenticated_client.vaults = Vaults(inner_client) + authenticated_client.groups = Groups(inner_client) authenticated_client._finalizer = weakref.finalize( cls, core.release_client, client_id diff --git a/src/onepassword/groups.py b/src/onepassword/groups.py index c3f4a7af..2d019593 100644 --- a/src/onepassword/groups.py +++ b/src/onepassword/groups.py @@ -1,6 +1,6 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import Core +from .core import InnerClient from pydantic import TypeAdapter from .types import Group, GroupGetParams @@ -10,15 +10,14 @@ class Groups: The Groups API holds all the operations the SDK client can perform on 1Password groups. """ - def __init__(self, client_id, core: Core): - self.client_id = client_id - self.core = core + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client async def get(self, group_id: str, group_params: GroupGetParams) -> Group: - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "GroupsGet", "parameters": { diff --git a/src/onepassword/items.py b/src/onepassword/items.py index c6b69018..591622a1 100644 --- a/src/onepassword/items.py +++ b/src/onepassword/items.py @@ -1,6 +1,6 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import Core +from .core import InnerClient from typing import List from pydantic import TypeAdapter from .items_shares import ItemsShares @@ -21,20 +21,19 @@ class Items: The Items API holds all operations the SDK client can perform on 1Password items. """ - def __init__(self, client_id, core: Core): - self.client_id = client_id - self.core = core - self.shares = ItemsShares(client_id, core) - self.files = ItemsFiles(client_id, core) + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client + self.shares = ItemsShares(inner_client) + self.files = ItemsFiles(inner_client) async def create(self, params: ItemCreateParams) -> Item: """ Create a new item. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsCreate", "parameters": {"params": params.model_dump(by_alias=True)}, @@ -52,10 +51,10 @@ async def create_all( """ Create items in batch, within a single vault. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsCreateAll", "parameters": { @@ -74,10 +73,10 @@ async def get(self, vault_id: str, item_id: str) -> Item: """ Get an item by vault and item ID """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsGet", "parameters": {"vault_id": vault_id, "item_id": item_id}, @@ -93,10 +92,10 @@ async def get_all(self, vault_id: str, item_ids: List[str]) -> ItemsGetAllRespon """ Get items by vault and their item IDs. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsGetAll", "parameters": {"vault_id": vault_id, "item_ids": item_ids}, @@ -112,10 +111,10 @@ async def put(self, item: Item) -> Item: """ Update an existing item. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsPut", "parameters": {"item": item.model_dump(by_alias=True)}, @@ -131,10 +130,10 @@ async def delete(self, vault_id: str, item_id: str) -> None: """ Delete an item. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsDelete", "parameters": {"vault_id": vault_id, "item_id": item_id}, @@ -151,10 +150,10 @@ async def delete_all( """ Delete items in batch, within a single vault. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsDeleteAll", "parameters": {"vault_id": vault_id, "item_ids": item_ids}, @@ -170,10 +169,10 @@ async def archive(self, vault_id: str, item_id: str) -> None: """ Archive an item. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsArchive", "parameters": {"vault_id": vault_id, "item_id": item_id}, @@ -188,10 +187,10 @@ async def list(self, vault_id: str, *filters: ItemListFilter) -> List[ItemOvervi """ List items based on filters. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsList", "parameters": { diff --git a/src/onepassword/items_files.py b/src/onepassword/items_files.py index d8c56ffa..6895226f 100644 --- a/src/onepassword/items_files.py +++ b/src/onepassword/items_files.py @@ -1,24 +1,23 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import Core +from .core import InnerClient from typing import List from pydantic import TypeAdapter from .types import DocumentCreateParams, FileAttributes, FileCreateParams, Item class ItemsFiles: - def __init__(self, client_id, core: Core): - self.client_id = client_id - self.core = core + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client async def attach(self, item: Item, file_params: FileCreateParams) -> Item: """ Attach files to Items """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsFilesAttach", "parameters": { @@ -37,10 +36,10 @@ async def read(self, vault_id: str, item_id: str, attr: FileAttributes) -> bytes """ Read file content from the Item """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsFilesRead", "parameters": { @@ -60,10 +59,10 @@ async def delete(self, item: Item, section_id: str, field_id: str) -> Item: """ Delete a field file from Item using the section and field IDs """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsFilesDelete", "parameters": { @@ -85,10 +84,10 @@ async def replace_document( """ Replace the document file within a document item """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsFilesReplaceDocument", "parameters": { diff --git a/src/onepassword/items_shares.py b/src/onepassword/items_shares.py index 715630d8..704ad1da 100644 --- a/src/onepassword/items_shares.py +++ b/src/onepassword/items_shares.py @@ -1,14 +1,14 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY +from .core import InnerClient from typing import List from pydantic import TypeAdapter from .types import Item, ItemShareAccountPolicy, ItemShareParams, ValidRecipient class ItemsShares: - def __init__(self, client_id, core): - self.client_id = client_id - self.core = core + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client async def get_account_policy( self, vault_id: str, item_id: str @@ -16,10 +16,10 @@ async def get_account_policy( """ Get the item sharing policy of your account. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsSharesGetAccountPolicy", "parameters": {"vault_id": vault_id, "item_id": item_id}, @@ -37,10 +37,10 @@ async def validate_recipients( """ Validate the recipients of an item sharing link. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsSharesValidateRecipients", "parameters": { @@ -61,10 +61,10 @@ async def create( """ Create a new item sharing link. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsSharesCreate", "parameters": { diff --git a/src/onepassword/secrets.py b/src/onepassword/secrets.py index f1c45475..8b46dfd8 100644 --- a/src/onepassword/secrets.py +++ b/src/onepassword/secrets.py @@ -1,6 +1,6 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import Core, UniffiCore +from .core import InnerClient, UniffiCore from typing import List from pydantic import TypeAdapter from .types import GeneratePasswordResponse, PasswordRecipe, ResolveAllResponse @@ -12,18 +12,17 @@ class Secrets: Use secret reference URIs to securely load secrets from 1Password: `op:///[/]/` """ - def __init__(self, client_id, core: Core): - self.client_id = client_id - self.core = core + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client async def resolve(self, secret_reference: str) -> str: """ Resolve returns the secret the provided secret reference points to. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "SecretsResolve", "parameters": {"secret_reference": secret_reference}, @@ -39,10 +38,10 @@ async def resolve_all(self, secret_references: List[str]) -> ResolveAllResponse: """ Resolve takes in a list of secret references and returns the secrets they point to or errors if any. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "SecretsResolveAll", "parameters": {"secret_references": secret_references}, diff --git a/src/onepassword/vaults.py b/src/onepassword/vaults.py index 45ff05ae..41c1acb0 100644 --- a/src/onepassword/vaults.py +++ b/src/onepassword/vaults.py @@ -1,6 +1,6 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import Core +from .core import InnerClient from typing import Optional, List from pydantic import TypeAdapter from .types import ( @@ -18,18 +18,17 @@ class Vaults: The Vaults API holds all the operations the SDK client can perform on 1Password vaults. """ - def __init__(self, client_id, core: Core): - self.client_id = client_id - self.core = core + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client async def list(self, params: Optional[VaultListParams] = None) -> List[VaultOverview]: """ List information about vaults that's configurable based on some input parameters. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "VaultsList", "parameters": {"params": params.model_dump(by_alias=True) if params else None}, @@ -42,10 +41,10 @@ async def list(self, params: Optional[VaultListParams] = None) -> List[VaultOver return response async def get_overview(self, vault_uuid: str) -> VaultOverview: - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "VaultsGetOverview", "parameters": {"vault_uuid": vault_uuid}, @@ -58,10 +57,10 @@ async def get_overview(self, vault_uuid: str) -> VaultOverview: return response async def get(self, vault_uuid: str, vault_params: VaultGetParams) -> Vault: - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "VaultsGet", "parameters": { @@ -79,10 +78,10 @@ async def get(self, vault_uuid: str, vault_params: VaultGetParams) -> Vault: async def grant_group_permissions( self, vault_id: str, group_permissions_list: List[GroupAccess] ) -> None: - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "VaultsGrantGroupPermissions", "parameters": { @@ -102,10 +101,10 @@ async def grant_group_permissions( async def update_group_permissions( self, group_permissions_list: List[GroupVaultAccess] ) -> None: - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "VaultsUpdateGroupPermissions", "parameters": { @@ -122,10 +121,10 @@ async def update_group_permissions( return None async def revoke_group_permissions(self, vault_id: str, group_id: str) -> None: - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "VaultsRevokeGroupPermissions", "parameters": {"vault_id": vault_id, "group_id": group_id}, From a2047690bdf4beadd550442a5d8ab5f98a16ca2b Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Thu, 6 Nov 2025 16:25:30 +0100 Subject: [PATCH 5/7] Fix errors --- src/onepassword/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/onepassword/core.py b/src/onepassword/core.py index 35d316e0..623d8cf4 100644 --- a/src/onepassword/core.py +++ b/src/onepassword/core.py @@ -1,6 +1,7 @@ +from __future__ import annotations import json import platform -from typing import Protocol +from typing import Any, Protocol from onepassword.desktop_core import DesktopCore from onepassword.errors import raise_typed_exception, DesktopSessionExpired @@ -20,7 +21,7 @@ def release_client(self, client_id: int) -> None: ... class InnerClient: client_id: int core: DesktopCore | UniffiCore - config: dict[str, any] + config: dict[str, Any] def __init__(self, client_id: int, core: "DesktopCore | UniffiCore", config: dict[str, any]): self.client_id = client_id From aa3ec9ed6d93fb4a1521781cf0532d25c012d7e1 Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Mon, 17 Nov 2025 12:08:36 +0100 Subject: [PATCH 6/7] Use porper exception name The new exception added now matches the naming convention for typed errors --- src/onepassword/core.py | 4 ++-- src/onepassword/errors.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/onepassword/core.py b/src/onepassword/core.py index 623d8cf4..a14825d9 100644 --- a/src/onepassword/core.py +++ b/src/onepassword/core.py @@ -3,7 +3,7 @@ import platform from typing import Any, Protocol from onepassword.desktop_core import DesktopCore -from onepassword.errors import raise_typed_exception, DesktopSessionExpired +from onepassword.errors import raise_typed_exception, DesktopSessionExpiredException # In empirical tests, we determined that maximum message size that can cross the FFI boundary # is ~128MB. Past this limit, FFI will throw an error and the program will crash. @@ -31,7 +31,7 @@ def __init__(self, client_id: int, core: "DesktopCore | UniffiCore", config: dic async def invoke(self, invoke_config: dict): try: return await self.core.invoke(invoke_config) - except DesktopSessionExpired as e: + except DesktopSessionExpiredException as e: new_client_id = await self.core.init_client(self.config) self.client_id = new_client_id invoke_config["invocation"]["clientId"] = self.client_id diff --git a/src/onepassword/errors.py b/src/onepassword/errors.py index 599e0757..811aebbe 100644 --- a/src/onepassword/errors.py +++ b/src/onepassword/errors.py @@ -3,7 +3,7 @@ import json -class DesktopSessionExpired(Exception): +class DesktopSessionExpiredException(Exception): def __init__(self, message): self.message = message super().__init__(self.message) @@ -25,7 +25,7 @@ def raise_typed_exception(e: Exception): message = typed_error.get("message") if error_name == "DesktopSessionExpired": - raise DesktopSessionExpired(message) + raise DesktopSessionExpiredException(message) elif error_name == "RateLimitExceeded": raise RateLimitExceededException(message) elif message is not None: From d9490c99f0c96886dc8f7a3fdbed5a304c68e466 Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Tue, 18 Nov 2025 14:09:54 +0100 Subject: [PATCH 7/7] Address nit --- src/onepassword/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onepassword/core.py b/src/onepassword/core.py index a14825d9..490a75ea 100644 --- a/src/onepassword/core.py +++ b/src/onepassword/core.py @@ -31,7 +31,7 @@ def __init__(self, client_id: int, core: "DesktopCore | UniffiCore", config: dic async def invoke(self, invoke_config: dict): try: return await self.core.invoke(invoke_config) - except DesktopSessionExpiredException as e: + except DesktopSessionExpiredException: new_client_id = await self.core.init_client(self.config) self.client_id = new_client_id invoke_config["invocation"]["clientId"] = self.client_id