Skip to content

Commit d9d503d

Browse files
committed
Merge branch 'beta' into sdk-core/2026-02-06-3d49ce29
2 parents d75181b + 0a5935b commit d9d503d

File tree

9 files changed

+420
-62
lines changed

9 files changed

+420
-62
lines changed

.github/workflows/validate.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
strategy:
2020
matrix:
2121
os: [ubuntu-latest, windows-latest, macos-latest]
22-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
22+
python-version: ["3.10", "3.11", "3.12", "3.13"]
2323
runs-on: ${{ matrix.os }}
2424
steps:
2525
- uses: actions/checkout@v3

example/desktop_app.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# [developer-docs.sdk.python.sdk-import]-start
2+
from onepassword import *
3+
import asyncio
4+
import os
5+
6+
7+
async def main():
8+
vault_id = os.environ.get("OP_VAULT_ID")
9+
if vault_id is None:
10+
raise Exception("OP_VAULT_ID is required")
11+
12+
# [developer-docs.sdk.python.client-initialization]-start
13+
# Connects to the 1Password desktop app.
14+
client = await Client.authenticate(
15+
auth=DesktopAuth(
16+
account_name="YourAccountNameAsShownInTheDesktopApp" # Set to your 1Password account name as shown at the top left sidebar of the app, or your account UUID.
17+
),
18+
# Set the following to your own integration name and version.
19+
integration_name="My 1Password Integration",
20+
integration_version="v1.0.0",
21+
)
22+
23+
# [developer-docs.sdk.python.list-vaults]-start
24+
vaults = await client.vaults.list()
25+
for vault in vaults:
26+
print(vault)
27+
# [developer-docs.sdk.python.list-vaults]-end
28+
29+
# [developer-docs.sdk.python.list-items]-start
30+
overviews = await client.items.list(vault_id)
31+
for overview in overviews:
32+
print(overview.title)
33+
# [developer-docs.sdk.python.list-items]-end
34+
35+
# [developer-docs.sdk.python.get-vault-overview]-start
36+
# Get vault overview
37+
vaultOverview = await client.vaults.get_overview(vault_id)
38+
print(vaultOverview)
39+
# [developer-docs.sdk.python.get-vault-overview]-end
40+
41+
# [developer-docs.sdk.python.get-vault-details]-start
42+
# Get vault details
43+
vault = await client.vaults.get(vaultOverview.id, VaultGetParams(accessors=False))
44+
print(vault)
45+
# [developer-docs.sdk.python.get-vault-details]-end
46+
47+
# [developer-docs.sdk.python.batch-create-items]-start
48+
items_to_create = []
49+
for i in range(1, 4):
50+
items_to_create.append(ItemCreateParams(
51+
title="My Login Item {}".format(i),
52+
category=ItemCategory.LOGIN,
53+
vault_id=vault.id,
54+
fields=[
55+
ItemField(
56+
id="username",
57+
title="username",
58+
field_type=ItemFieldType.TEXT,
59+
value="mynameisjeff",
60+
),
61+
ItemField(
62+
id="password",
63+
title="password",
64+
field_type=ItemFieldType.CONCEALED,
65+
value="jeff",
66+
),
67+
ItemField(
68+
id="onetimepassword",
69+
title="one-time-password",
70+
field_type=ItemFieldType.TOTP,
71+
section_id="totpsection",
72+
value="otpauth://totp/my-example-otp?secret=jncrjgbdjnrncbjsr&issuer=1Password",
73+
),
74+
],
75+
sections=[
76+
ItemSection(
77+
id="", title=""),
78+
ItemSection(
79+
id="totpsection", title=""),
80+
],
81+
tags=[
82+
"test tag 1", "test tag 2"],
83+
websites=[
84+
Website(
85+
label="my custom website",
86+
url="https://example.com",
87+
autofill_behavior=AutofillBehavior.NEVER,
88+
)
89+
],
90+
))
91+
92+
# Create all items in the same vault in a single batch
93+
batchCreateResponse = await client.items.create_all(vault.id, items_to_create)
94+
95+
item_ids = []
96+
for res in batchCreateResponse.individual_responses:
97+
if res.content is not None:
98+
print('Created item "{}" ({})'.format(
99+
res.content.title, res.content.id))
100+
item_ids.append(res.content.id)
101+
elif res.error is not None:
102+
print("[Batch create] Something went wrong: {}".format(res.error))
103+
# [developer-docs.sdk.python.batch-create-items]-end
104+
105+
# [developer-docs.sdk.python.batch-get-items]-start
106+
# Get multiple items form the same vault in a single batch
107+
batchGetReponse = await client.items.get_all(vault.id, item_ids)
108+
for res in batchGetReponse.individual_responses:
109+
if res.content is not None:
110+
print('Obtained item "{}" ({})'.format(
111+
res.content.title, res.content.id))
112+
elif res.error is not None:
113+
print("[Batch get] Something went wrong: {}".format(res.error))
114+
# [developer-docs.sdk.python.batch-get-items]-end
115+
116+
# [developer-docs.sdk.python.batch-delete-items]-start
117+
# Delete multiple items from the same vault in a single batch
118+
batchDeleteResponse = await client.items.delete_all(vault.id, item_ids)
119+
for id, res in batchDeleteResponse.individual_responses.items():
120+
if res.error is not None:
121+
print("[Batch delete] Something went wrong: {}".format(res.error))
122+
else:
123+
print("Deleted item {}".format(id))
124+
# [developer-docs.sdk.python.batch-delete-items]-end
125+
126+
group_id = os.environ.get("OP_GROUP_ID")
127+
if group_id is None:
128+
raise Exception("OP_GROUP_ID is required")
129+
130+
# [developer-docs.sdk.python.get-group]-start
131+
# Get a group
132+
group = await client.groups.get(group_id, GroupGetParams(vaultPermissions=False))
133+
print(group)
134+
# [developer-docs.sdk.python.get-group]-end
135+
136+
137+
if __name__ == "__main__":
138+
asyncio.run(main())

src/onepassword/build_number.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
SDK_BUILD_NUMBER = "0030201"
1+
SDK_BUILD_NUMBER = "0040002"

src/onepassword/core.py

Lines changed: 90 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,98 @@
1+
from __future__ import annotations
12
import json
23
import platform
3-
4-
from onepassword.errors import raise_typed_exception
4+
from typing import Any, Protocol
5+
from onepassword.desktop_core import DesktopCore
6+
from onepassword.errors import raise_typed_exception, DesktopSessionExpiredException
57

68
# In empirical tests, we determined that maximum message size that can cross the FFI boundary
79
# is ~128MB. Past this limit, FFI will throw an error and the program will crash.
810
# 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
911
MESSAGE_LIMIT = 50 * 1024 * 1024
1012

11-
machine_arch = platform.machine().lower()
12-
13-
if machine_arch in ["x86_64", "amd64"]:
14-
import onepassword.lib.x86_64.op_uniffi_core as core
15-
elif machine_arch in ["aarch64", "arm64"]:
16-
import onepassword.lib.aarch64.op_uniffi_core as core
17-
else:
18-
raise ImportError(
19-
f"Your machine's architecture is not currently supported: {machine_arch}"
20-
)
21-
22-
23-
# InitClient creates a client instance in the current core module and returns its unique ID.
24-
async def _init_client(client_config):
25-
try:
26-
return await core.init_client(json.dumps(client_config))
27-
except Exception as e:
28-
raise_typed_exception(e)
29-
30-
31-
# Invoke calls specified business logic from the SDK core.
32-
async def _invoke(invoke_config):
33-
serialized_config = json.dumps(invoke_config)
34-
if len(serialized_config.encode()) > MESSAGE_LIMIT:
35-
raise ValueError(
36-
f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, please contact 1Password at support@1password.com or https://developer.1password.com/joinslack if you need help."
37-
)
38-
try:
39-
return await core.invoke(serialized_config)
40-
except Exception as e:
41-
raise_typed_exception(e)
42-
43-
44-
# Invoke calls specified business logic from the SDK core.
45-
def _invoke_sync(invoke_config):
46-
serialized_config = json.dumps(invoke_config)
47-
if len(serialized_config.encode()) > MESSAGE_LIMIT:
48-
raise ValueError(
49-
f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, please contact 1Password at support@1password.com or https://developer.1password.com/joinslack if you need help."
50-
)
51-
try:
52-
return core.invoke_sync(serialized_config)
53-
except Exception as e:
54-
raise_typed_exception(e)
55-
56-
57-
# ReleaseClient releases memory in the SDK core associated with the given client ID.
58-
def _release_client(client_id):
59-
return core.release_client(json.dumps(client_id))
13+
14+
class Core(Protocol):
15+
async def init_client(self, client_config: dict) -> str: ...
16+
async def invoke(self, invoke_config: dict) -> str: ...
17+
def invoke_sync(self, invoke_config: dict) -> str: ...
18+
def release_client(self, client_id: int) -> None: ...
19+
20+
21+
class InnerClient:
22+
client_id: int
23+
core: DesktopCore | UniffiCore
24+
config: dict[str, Any]
25+
26+
def __init__(self, client_id: int, core: "DesktopCore | UniffiCore", config: dict[str, any]):
27+
self.client_id = client_id
28+
self.core = core
29+
self.config = config
30+
31+
async def invoke(self, invoke_config: dict):
32+
try:
33+
return await self.core.invoke(invoke_config)
34+
except DesktopSessionExpiredException:
35+
new_client_id = await self.core.init_client(self.config)
36+
self.client_id = new_client_id
37+
invoke_config["invocation"]["clientId"] = self.client_id
38+
return await self.core.invoke(invoke_config)
39+
except Exception as e:
40+
raise e
41+
42+
43+
class UniffiCore:
44+
def __init__(self):
45+
machine_arch = platform.machine().lower()
46+
47+
if machine_arch in ["x86_64", "amd64"]:
48+
import onepassword.lib.x86_64.op_uniffi_core as core
49+
elif machine_arch in ["aarch64", "arm64"]:
50+
import onepassword.lib.aarch64.op_uniffi_core as core
51+
else:
52+
raise ImportError(
53+
f"Your machine's architecture is not currently supported: {machine_arch}"
54+
)
55+
56+
self.core = core
57+
58+
async def init_client(self, client_config: dict):
59+
"""Creates a client instance in the current core module and returns its unique ID."""
60+
try:
61+
return await self.core.init_client(json.dumps(client_config))
62+
except Exception as e:
63+
raise_typed_exception(e)
64+
65+
async def invoke(self, invoke_config: dict):
66+
"""Invoke business logic asynchronously."""
67+
serialized_config = json.dumps(invoke_config)
68+
if len(serialized_config.encode()) > MESSAGE_LIMIT:
69+
raise ValueError(
70+
f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, "
71+
"please contact 1Password at support@1password.com or "
72+
"https://developer.1password.com/joinslack if you need help."
73+
)
74+
try:
75+
return await self.core.invoke(serialized_config)
76+
except Exception as e:
77+
raise_typed_exception(e)
78+
79+
def invoke_sync(self, invoke_config: dict):
80+
"""Invoke business logic synchronously."""
81+
serialized_config = json.dumps(invoke_config)
82+
if len(serialized_config.encode()) > MESSAGE_LIMIT:
83+
raise ValueError(
84+
f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, "
85+
"please contact 1Password at support@1password.com or "
86+
"https://developer.1password.com/joinslack if you need help."
87+
)
88+
try:
89+
return self.core.invoke_sync(serialized_config)
90+
except Exception as e:
91+
raise_typed_exception(e)
92+
93+
def release_client(self, client_id: int):
94+
"""Releases memory in the SDK core associated with the given client ID."""
95+
try:
96+
return self.core.release_client(json.dumps(client_id))
97+
except Exception as e:
98+
raise_typed_exception(e)

src/onepassword/defaults.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,19 @@
99
DEFAULT_REQUEST_LIBRARY_VERSION = "0.11.24"
1010
DEFAULT_OS_VERSION = "0.0.0"
1111

12+
class DesktopAuth:
13+
def __init__(self, account_name: str):
14+
"""
15+
Initialize a DesktopAuth instance.
16+
17+
Args:
18+
account_name (str): The name of the account.
19+
"""
20+
self.account_name = account_name
1221

1322
# Generates a configuration dictionary with the user's parameters
14-
def new_default_config(auth, integration_name, integration_version):
23+
def new_default_config(auth: DesktopAuth | str, integration_name, integration_version):
1524
client_config_dict = {
16-
"serviceAccountToken": auth,
1725
"programmingLanguage": SDK_LANGUAGE,
1826
"sdkVersion": SDK_VERSION,
1927
"integrationName": integration_name,
@@ -24,4 +32,7 @@ def new_default_config(auth, integration_name, integration_version):
2432
"osVersion": DEFAULT_OS_VERSION,
2533
"architecture": platform.machine(),
2634
}
35+
if not isinstance(auth, DesktopAuth):
36+
client_config_dict["serviceAccountToken"] = auth
37+
2738
return client_config_dict

0 commit comments

Comments
 (0)