Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python 3.9 will reach EOL 30th of October anyway: https://devguide.python.org/versions/

runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
Expand Down
32 changes: 32 additions & 0 deletions example/desktop_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# [developer-docs.sdk.python.sdk-import]-start
from onepassword import *
import asyncio


async def main():
# [developer-docs.sdk.python.client-initialization]-start
# Connects to the 1Password desktop app.
client = await Client.authenticate(
auth=DesktopAuth(
account_name="AndiTituTest" # Set to your 1Password account name.
),
# Set the following to your own integration name and version.
integration_name="My 1Password Integration",
integration_version="v1.0.0",
)

# [developer-docs.sdk.python.list-vaults]-start
vaults = await client.vaults.list()
for vault in vaults:
print(vault)
# [developer-docs.sdk.python.list-vaults]-end

# [developer-docs.sdk.python.list-items]-start
overviews = await client.items.list("xw33qlvug6moegr3wkk5zkenoa")
for overview in overviews:
print(overview.title)
# [developer-docs.sdk.python.list-items]-end


if __name__ == "__main__":
asyncio.run(main())
3 changes: 2 additions & 1 deletion src/onepassword/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Code generated by op-codegen - DO NO EDIT MANUALLY

from .client import Client
from .client import Client, DesktopAuth
from .defaults import DEFAULT_INTEGRATION_NAME, DEFAULT_INTEGRATION_VERSION
from .types import * # noqa F403
from .errors import * # noqa F403
Expand All @@ -20,6 +20,7 @@
"Vaults",
"DEFAULT_INTEGRATION_NAME",
"DEFAULT_INTEGRATION_VERSION",
"DesktopAuth",
]

for name, obj in inspect.getmembers(sys.modules["onepassword.types"]):
Expand All @@ -30,7 +31,7 @@
and inspect.getmodule(obj) == sys.modules["onepassword.types"]
)
or isinstance(obj, int)
or type(obj) == typing._LiteralGenericAlias

Check failure on line 34 in src/onepassword/__init__.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (E721)

src/onepassword/__init__.py:34:12: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks

Check failure on line 34 in src/onepassword/__init__.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (E721)

src/onepassword/__init__.py:34:12: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
):
__all__.append(name)

Expand Down
24 changes: 15 additions & 9 deletions src/onepassword/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

from __future__ import annotations
import weakref
from .core import _init_client, _release_client
from .defaults import new_default_config
from .core import Core, UniffiCore

Check failure on line 5 in src/onepassword/client.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (F401)

src/onepassword/client.py:5:19: F401 `.core.Core` imported but unused

Check failure on line 5 in src/onepassword/client.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (F401)

src/onepassword/client.py:5:19: F401 `.core.Core` imported but unused
from .desktop_core import DesktopCore
from .defaults import new_default_config, DesktopAuth
from .secrets import Secrets
from .items import Items
from .vaults import Vaults
Expand All @@ -16,23 +17,28 @@

@classmethod
async def authenticate(
cls, auth: str, integration_name: str, integration_version: str
cls, auth: str | DesktopAuth, integration_name: str, integration_version: str
) -> Client:
config = new_default_config(
auth=auth or "",
auth=auth,
integration_name=integration_name,
integration_version=integration_version,
)

client_id = int(await _init_client(config))
if isinstance(auth, DesktopAuth):
core = DesktopCore(auth.account_name)
else:
core = UniffiCore()

client_id = int(await core.init_client(config))

authenticated_client = cls()

authenticated_client.secrets = Secrets(client_id)
authenticated_client.items = Items(client_id)
authenticated_client.vaults = Vaults(client_id)
authenticated_client.secrets = Secrets(client_id, core)
authenticated_client.items = Items(client_id, core)
authenticated_client.vaults = Vaults(client_id, core)
authenticated_client._finalizer = weakref.finalize(
cls, _release_client, client_id
cls, core.release_client, client_id
)

return authenticated_client
113 changes: 63 additions & 50 deletions src/onepassword/core.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,72 @@
import json
import platform

from typing import Protocol
from onepassword.errors import raise_typed_exception

# 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

machine_arch = platform.machine().lower()

if machine_arch in ["x86_64", "amd64"]:
import onepassword.lib.x86_64.op_uniffi_core as core
elif machine_arch in ["aarch64", "arm64"]:
import onepassword.lib.aarch64.op_uniffi_core as core
else:
raise ImportError(
f"Your machine's architecture is not currently supported: {machine_arch}"
)


# InitClient creates a client instance in the current core module and returns its unique ID.
async def _init_client(client_config):
try:
return await core.init_client(json.dumps(client_config))
except Exception as e:
raise_typed_exception(e)


# Invoke calls specified business logic from the SDK core.
async def _invoke(invoke_config):
serialized_config = json.dumps(invoke_config)
if len(serialized_config.encode()) > MESSAGE_LIMIT:
raise ValueError(
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."
)
try:
return await core.invoke(serialized_config)
except Exception as e:
raise_typed_exception(e)


# Invoke calls specified business logic from the SDK core.
def _invoke_sync(invoke_config):
serialized_config = json.dumps(invoke_config)
if len(serialized_config.encode()) > MESSAGE_LIMIT:
raise ValueError(
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."
)
try:
return core.invoke_sync(serialized_config)
except Exception as e:
raise_typed_exception(e)


# ReleaseClient releases memory in the SDK core associated with the given client ID.
def _release_client(client_id):
return core.release_client(json.dumps(client_id))
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 UniffiCore:
def __init__(self):
machine_arch = platform.machine().lower()

if machine_arch in ["x86_64", "amd64"]:
import onepassword.lib.x86_64.op_uniffi_core as core
elif machine_arch in ["aarch64", "arm64"]:
import onepassword.lib.aarch64.op_uniffi_core as core
else:
raise ImportError(
f"Your machine's architecture is not currently supported: {machine_arch}"
)

self.core = core

async def init_client(self, client_config: dict):
"""Creates a client instance in the current core module and returns its unique ID."""
try:
return await self.core.init_client(json.dumps(client_config))
except Exception as e:
raise_typed_exception(e)

async def invoke(self, invoke_config: dict):
"""Invoke business logic asynchronously."""
serialized_config = json.dumps(invoke_config)
if len(serialized_config.encode()) > MESSAGE_LIMIT:
raise ValueError(
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."
)
try:
return await self.core.invoke(serialized_config)
except Exception as e:
raise_typed_exception(e)

def invoke_sync(self, invoke_config: dict):
"""Invoke business logic synchronously."""
serialized_config = json.dumps(invoke_config)
if len(serialized_config.encode()) > MESSAGE_LIMIT:
raise ValueError(
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."
)
try:
return self.core.invoke_sync(serialized_config)
except Exception as e:
raise_typed_exception(e)

def release_client(self, client_id: int):
"""Releases memory in the SDK core associated with the given client ID."""
try:
return self.core.release_client(json.dumps(client_id))
except Exception as e:
raise_typed_exception(e)
15 changes: 13 additions & 2 deletions src/onepassword/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@
DEFAULT_REQUEST_LIBRARY_VERSION = "0.11.24"
DEFAULT_OS_VERSION = "0.0.0"

class DesktopAuth:
def __init__(self, account_name: str):
"""
Initialize a DesktopAuth instance.

Args:
account_name (str): The name of the account.
"""
self.account_name = account_name

# Generates a configuration dictionary with the user's parameters
def new_default_config(auth, integration_name, integration_version):
def new_default_config(auth: DesktopAuth | str, integration_name, integration_version):
client_config_dict = {
"serviceAccountToken": auth,
"programmingLanguage": SDK_LANGUAGE,
"sdkVersion": SDK_VERSION,
"integrationName": integration_name,
Expand All @@ -24,4 +32,7 @@ def new_default_config(auth, integration_name, integration_version):
"osVersion": DEFAULT_OS_VERSION,
"architecture": platform.machine(),
}
if isinstance(auth, str):
client_config_dict["serviceAccountToken"] = auth

return client_config_dict
99 changes: 99 additions & 0 deletions src/onepassword/desktop_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import ctypes
import json
import os
import base64
from ctypes import c_uint8, c_size_t, c_int32, POINTER, byref, c_void_p

Check failure on line 5 in src/onepassword/desktop_core.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (F401)

src/onepassword/desktop_core.py:5:64: F401 `ctypes.c_void_p` imported but unused

Check failure on line 5 in src/onepassword/desktop_core.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (F401)

src/onepassword/desktop_core.py:5:64: F401 `ctypes.c_void_p` imported but unused
from .core import UniffiCore

Check failure on line 6 in src/onepassword/desktop_core.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (F401)

src/onepassword/desktop_core.py:6:19: F401 `.core.UniffiCore` imported but unused

Check failure on line 6 in src/onepassword/desktop_core.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (F401)

src/onepassword/desktop_core.py:6:19: F401 `.core.UniffiCore` imported but unused
from onepassword.errors import raise_typed_exception


def find_1password_lib_path():
locations = [
"/Users/andititu/core/target/debug/libop_sdk_ipc_client.dylib"
]

for lib_path in locations:
if os.path.exists(lib_path):
return lib_path

raise FileNotFoundError("1Password desktop application not found")

class DesktopCore:
def __init__(self, account_name: str):
# Determine the path to the desktop app.
path = find_1password_lib_path()

self.lib = ctypes.CDLL(path)
self.account_name = account_name

# Bind the Rust-exported functions
self.send_message = self.lib.op_sdk_ipc_send_message
self.send_message.argtypes = [
POINTER(c_uint8), # msg_ptr
c_size_t, # msg_len
POINTER(POINTER(c_uint8)), # out_buf
POINTER(c_size_t), # out_len
POINTER(c_size_t), # out_cap
]
self.send_message.restype = c_int32

self.free_message = self.lib.op_sdk_ipc_free_response
self.free_message.argtypes = [POINTER(c_uint8), c_size_t, c_size_t]
self.free_message.restype = None

def call_shared_library(self, payload: str, operation_kind: str) -> bytes:
# Prepare the input
encoded_payload = base64.b64encode(payload.encode("utf-8")).decode("utf-8")
data = {
"kind": operation_kind,
"account_name": self.account_name,
"payload": encoded_payload,
}
message = json.dumps(data).encode("utf-8")

# Prepare output parameters
out_buf = POINTER(c_uint8)()
out_len = c_size_t()
out_cap = c_size_t()

ret = self.send_message(
(ctypes.cast(message, POINTER(c_uint8))),
len(message),
byref(out_buf),
byref(out_len),
byref(out_cap),
)

if ret != 0:
raise RuntimeError(f"send_message failed with code {ret}. Please make sure the Desktop app intehration setting is enabled, or contact 1Password support.")

# Copy bytes into Python
data = ctypes.string_at(out_buf, out_len.value)

# Free memory via Rust's exported function
self.free_message(out_buf, out_len, out_cap)

parsed = json.loads(data)
payload = bytes(parsed.get("payload", [])).decode("utf-8")

success = parsed.get("success", False)
if not success:
raise_typed_exception(Exception(str(payload)))

return payload

async def init_client(self, config: dict) -> int:
payload = json.dumps(config)
resp = self.call_shared_library(payload, "init_client")
return json.loads(resp)

async def invoke(self, invoke_config: dict) -> str:
payload = json.dumps(invoke_config)
return self.call_shared_library(payload, "invoke")

def release_client(self, client_id: int):
payload = json.dumps(client_id)
try:
self.call_shared_library(payload, "release_client")
except Exception as e:
print(f"failed to release client: {e}")
Loading