Skip to content

Commit e552f5f

Browse files
authored
Merge pull request #187 from 1Password/sdks-for-desktop-integrations
Prepare SDK beta release 0.4.0b1
2 parents c29275d + ab960f2 commit e552f5f

File tree

19 files changed

+779
-111
lines changed

19 files changed

+779
-111
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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# [developer-docs.sdk.python.sdk-import]-start
2+
from onepassword import *
3+
import asyncio
4+
5+
6+
async def main():
7+
# [developer-docs.sdk.python.client-initialization]-start
8+
# Connects to the 1Password desktop app.
9+
client = await Client.authenticate(
10+
auth=DesktopAuth(
11+
account_name="YouAccountNameAsShownInDesktopApp" # Set to your 1Password account name.
12+
),
13+
# Set the following to your own integration name and version.
14+
integration_name="My 1Password Integration",
15+
integration_version="v1.0.0",
16+
)
17+
18+
# [developer-docs.sdk.python.list-vaults]-start
19+
vaults = await client.vaults.list()
20+
for vault in vaults:
21+
print(vault)
22+
# [developer-docs.sdk.python.list-vaults]-end
23+
24+
# [developer-docs.sdk.python.list-items]-start
25+
overviews = await client.items.list(vault_id=vaults[0].id)
26+
for overview in overviews:
27+
print(overview.title)
28+
# [developer-docs.sdk.python.list-items]-end
29+
30+
31+
if __name__ == "__main__":
32+
asyncio.run(main())

src/onepassword/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# Code generated by op-codegen - DO NO EDIT MANUALLY
22

3-
from .client import Client
3+
from .client import Client, DesktopAuth
44
from .defaults import DEFAULT_INTEGRATION_NAME, DEFAULT_INTEGRATION_VERSION
55
from .types import * # noqa F403
66
from .errors import * # noqa F403
77
from .secrets import Secrets
88
from .items import Items
99
from .vaults import Vaults
10+
from .groups import Groups
1011

1112

1213
import sys
@@ -18,8 +19,10 @@
1819
"Secrets",
1920
"Items",
2021
"Vaults",
22+
"Groups",
2123
"DEFAULT_INTEGRATION_NAME",
2224
"DEFAULT_INTEGRATION_VERSION",
25+
"DesktopAuth",
2326
]
2427

2528
for name, obj in inspect.getmembers(sys.modules["onepassword.types"]):

src/onepassword/client.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,47 @@
22

33
from __future__ import annotations
44
import weakref
5-
from .core import _init_client, _release_client
6-
from .defaults import new_default_config
5+
from .core import UniffiCore
6+
from .desktop_core import DesktopCore
7+
from .defaults import new_default_config, DesktopAuth
78
from .secrets import Secrets
89
from .items import Items
910
from .vaults import Vaults
11+
from .groups import Groups
1012

1113

1214
class Client:
1315
secrets: Secrets
1416
items: Items
1517
vaults: Vaults
18+
groups: Groups
1619

1720
@classmethod
1821
async def authenticate(
19-
cls, auth: str, integration_name: str, integration_version: str
22+
cls, auth: str | DesktopAuth, integration_name: str, integration_version: str
2023
) -> Client:
2124
config = new_default_config(
22-
auth=auth or "",
25+
auth=auth,
2326
integration_name=integration_name,
2427
integration_version=integration_version,
2528
)
2629

27-
client_id = int(await _init_client(config))
30+
if isinstance(auth, DesktopAuth):
31+
core = DesktopCore(auth.account_name)
32+
else:
33+
core = UniffiCore()
34+
35+
client_id = int(await core.init_client(config))
2836

2937
authenticated_client = cls()
3038

31-
authenticated_client.secrets = Secrets(client_id)
32-
authenticated_client.items = Items(client_id)
33-
authenticated_client.vaults = Vaults(client_id)
39+
authenticated_client.secrets = Secrets(client_id, core)
40+
authenticated_client.items = Items(client_id, core)
41+
authenticated_client.vaults = Vaults(client_id, core)
42+
authenticated_client.groups = Groups(client_id, core)
43+
3444
authenticated_client._finalizer = weakref.finalize(
35-
cls, _release_client, client_id
45+
cls, core.release_client, client_id
3646
)
3747

3848
return authenticated_client

src/onepassword/core.py

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,72 @@
11
import json
22
import platform
3-
3+
from typing import Protocol
44
from onepassword.errors import raise_typed_exception
55

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

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 [email protected] 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 [email protected] 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))
11+
class Core(Protocol):
12+
async def init_client(self, client_config: dict) -> str: ...
13+
async def invoke(self, invoke_config: dict) -> str: ...
14+
def invoke_sync(self, invoke_config: dict) -> str: ...
15+
def release_client(self, client_id: int) -> None: ...
16+
17+
class UniffiCore:
18+
def __init__(self):
19+
machine_arch = platform.machine().lower()
20+
21+
if machine_arch in ["x86_64", "amd64"]:
22+
import onepassword.lib.x86_64.op_uniffi_core as core
23+
elif machine_arch in ["aarch64", "arm64"]:
24+
import onepassword.lib.aarch64.op_uniffi_core as core
25+
else:
26+
raise ImportError(
27+
f"Your machine's architecture is not currently supported: {machine_arch}"
28+
)
29+
30+
self.core = core
31+
32+
async def init_client(self, client_config: dict):
33+
"""Creates a client instance in the current core module and returns its unique ID."""
34+
try:
35+
return await self.core.init_client(json.dumps(client_config))
36+
except Exception as e:
37+
raise_typed_exception(e)
38+
39+
async def invoke(self, invoke_config: dict):
40+
"""Invoke business logic asynchronously."""
41+
serialized_config = json.dumps(invoke_config)
42+
if len(serialized_config.encode()) > MESSAGE_LIMIT:
43+
raise ValueError(
44+
f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, "
45+
"please contact 1Password at [email protected] or "
46+
"https://developer.1password.com/joinslack if you need help."
47+
)
48+
try:
49+
return await self.core.invoke(serialized_config)
50+
except Exception as e:
51+
raise_typed_exception(e)
52+
53+
def invoke_sync(self, invoke_config: dict):
54+
"""Invoke business logic synchronously."""
55+
serialized_config = json.dumps(invoke_config)
56+
if len(serialized_config.encode()) > MESSAGE_LIMIT:
57+
raise ValueError(
58+
f"message size exceeds the limit of {MESSAGE_LIMIT} bytes, "
59+
"please contact 1Password at [email protected] or "
60+
"https://developer.1password.com/joinslack if you need help."
61+
)
62+
try:
63+
return self.core.invoke_sync(serialized_config)
64+
except Exception as e:
65+
raise_typed_exception(e)
66+
67+
def release_client(self, client_id: int):
68+
"""Releases memory in the SDK core associated with the given client ID."""
69+
try:
70+
return self.core.release_client(json.dumps(client_id))
71+
except Exception as e:
72+
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

src/onepassword/desktop_core.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import ctypes
2+
import json
3+
import os
4+
import platform
5+
import base64
6+
from pathlib import Path
7+
from ctypes import c_uint8, c_size_t, c_int32, POINTER, byref, c_void_p
8+
from onepassword.errors import raise_typed_exception
9+
10+
11+
def find_1password_lib_path():
12+
os_name = platform.system()
13+
14+
# Define paths based on OS
15+
if os_name == "Darwin": # macOS
16+
locations = [
17+
"/Applications/1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib",
18+
str(Path.home() / "Applications/1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib"),
19+
]
20+
elif os_name == "Linux":
21+
locations = [
22+
"/usr/bin/1password/libop_sdk_ipc_client.so",
23+
"/opt/1Password/libop_sdk_ipc_client.so",
24+
"/snap/bin/1password/libop_sdk_ipc_client.so",
25+
]
26+
else:
27+
raise OSError(f"Unsupported operating system: {os_name}")
28+
29+
for lib_path in locations:
30+
if os.path.exists(lib_path):
31+
return lib_path
32+
33+
raise FileNotFoundError("1Password desktop application not found")
34+
35+
class DesktopCore:
36+
def __init__(self, account_name: str):
37+
# Determine the path to the desktop app.
38+
path = find_1password_lib_path()
39+
40+
self.lib = ctypes.CDLL(path)
41+
self.account_name = account_name
42+
43+
# Bind the Rust-exported functions
44+
self.send_message = self.lib.op_sdk_ipc_send_message
45+
self.send_message.argtypes = [
46+
POINTER(c_uint8), # msg_ptr
47+
c_size_t, # msg_len
48+
POINTER(POINTER(c_uint8)), # out_buf
49+
POINTER(c_size_t), # out_len
50+
POINTER(c_size_t), # out_cap
51+
]
52+
self.send_message.restype = c_int32
53+
54+
self.free_message = self.lib.op_sdk_ipc_free_response
55+
self.free_message.argtypes = [POINTER(c_uint8), c_size_t, c_size_t]
56+
self.free_message.restype = None
57+
58+
def call_shared_library(self, payload: str, operation_kind: str) -> bytes:
59+
# Prepare the input
60+
encoded_payload = base64.b64encode(payload.encode("utf-8")).decode("utf-8")
61+
data = {
62+
"kind": operation_kind,
63+
"account_name": self.account_name,
64+
"payload": encoded_payload,
65+
}
66+
message = json.dumps(data).encode("utf-8")
67+
68+
# Prepare output parameters
69+
out_buf = POINTER(c_uint8)()
70+
out_len = c_size_t()
71+
out_cap = c_size_t()
72+
73+
ret = self.send_message(
74+
(ctypes.cast(message, POINTER(c_uint8))),
75+
len(message),
76+
byref(out_buf),
77+
byref(out_len),
78+
byref(out_cap),
79+
)
80+
81+
if ret != 0:
82+
raise RuntimeError(f"send_message failed with code {ret}. Please make sure the Desktop app integration setting is enabled, or contact 1Password support.")
83+
84+
# Copy bytes into Python
85+
data = ctypes.string_at(out_buf, out_len.value)
86+
87+
# Free memory via Rust's exported function
88+
self.free_message(out_buf, out_len, out_cap)
89+
90+
parsed = json.loads(data)
91+
payload = bytes(parsed.get("payload", [])).decode("utf-8")
92+
93+
success = parsed.get("success", False)
94+
if not success:
95+
raise_typed_exception(Exception(str(payload)))
96+
97+
return payload
98+
99+
async def init_client(self, config: dict) -> int:
100+
payload = json.dumps(config)
101+
resp = self.call_shared_library(payload, "init_client")
102+
return json.loads(resp)
103+
104+
async def invoke(self, invoke_config: dict) -> str:
105+
payload = json.dumps(invoke_config)
106+
return self.call_shared_library(payload, "invoke")
107+
108+
def release_client(self, client_id: int):
109+
payload = json.dumps(client_id)
110+
try:
111+
self.call_shared_library(payload, "release_client")
112+
except Exception as e:
113+
print(f"failed to release client: {e}")

0 commit comments

Comments
 (0)