diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b68c6c8..8926936 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 diff --git a/example/desktop_app.py b/example/desktop_app.py new file mode 100644 index 0000000..b66caab --- /dev/null +++ b/example/desktop_app.py @@ -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="YouAccountNameAsShownInDesktopApp" # 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(vault_id=vaults[0].id) + for overview in overviews: + print(overview.title) + # [developer-docs.sdk.python.list-items]-end + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/src/onepassword/__init__.py b/src/onepassword/__init__.py index 040ae01..473334e 100644 --- a/src/onepassword/__init__.py +++ b/src/onepassword/__init__.py @@ -1,12 +1,13 @@ # 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 from .secrets import Secrets from .items import Items from .vaults import Vaults +from .groups import Groups import sys @@ -18,8 +19,10 @@ "Secrets", "Items", "Vaults", + "Groups", "DEFAULT_INTEGRATION_NAME", "DEFAULT_INTEGRATION_VERSION", + "DesktopAuth", ] for name, obj in inspect.getmembers(sys.modules["onepassword.types"]): diff --git a/src/onepassword/client.py b/src/onepassword/client.py index ae5377f..0b3d319 100644 --- a/src/onepassword/client.py +++ b/src/onepassword/client.py @@ -2,37 +2,47 @@ from __future__ import annotations import weakref -from .core import _init_client, _release_client -from .defaults import new_default_config +from .core import UniffiCore +from .desktop_core import DesktopCore +from .defaults import new_default_config, DesktopAuth from .secrets import Secrets from .items import Items from .vaults import Vaults +from .groups import Groups class Client: secrets: Secrets items: Items vaults: Vaults + groups: Groups @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.groups = Groups(client_id, core) + authenticated_client._finalizer = weakref.finalize( - cls, _release_client, client_id + cls, core.release_client, client_id ) return authenticated_client diff --git a/src/onepassword/core.py b/src/onepassword/core.py index b1592e1..d47068a 100644 --- a/src/onepassword/core.py +++ b/src/onepassword/core.py @@ -1,6 +1,6 @@ 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 @@ -8,52 +8,65 @@ # 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 support@1password.com 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 support@1password.com 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 support@1password.com 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 support@1password.com 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) diff --git a/src/onepassword/defaults.py b/src/onepassword/defaults.py index cfa4048..1cd626a 100644 --- a/src/onepassword/defaults.py +++ b/src/onepassword/defaults.py @@ -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, @@ -24,4 +32,7 @@ def new_default_config(auth, integration_name, integration_version): "osVersion": DEFAULT_OS_VERSION, "architecture": platform.machine(), } + if not isinstance(auth, DesktopAuth): + client_config_dict["serviceAccountToken"] = auth + return client_config_dict diff --git a/src/onepassword/desktop_core.py b/src/onepassword/desktop_core.py new file mode 100644 index 0000000..b0e1b5e --- /dev/null +++ b/src/onepassword/desktop_core.py @@ -0,0 +1,113 @@ +import ctypes +import json +import os +import platform +import base64 +from pathlib import Path +from ctypes import c_uint8, c_size_t, c_int32, POINTER, byref, c_void_p +from onepassword.errors import raise_typed_exception + + +def find_1password_lib_path(): + os_name = platform.system() + + # Define paths based on OS + if os_name == "Darwin": # macOS + locations = [ + "/Applications/1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib", + str(Path.home() / "Applications/1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib"), + ] + elif os_name == "Linux": + locations = [ + "/usr/bin/1password/libop_sdk_ipc_client.so", + "/opt/1Password/libop_sdk_ipc_client.so", + "/snap/bin/1password/libop_sdk_ipc_client.so", + ] + else: + raise OSError(f"Unsupported operating system: {os_name}") + + 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 integration 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}") diff --git a/src/onepassword/groups.py b/src/onepassword/groups.py new file mode 100644 index 0000000..c3f4a7a --- /dev/null +++ b/src/onepassword/groups.py @@ -0,0 +1,34 @@ +# Code generated by op-codegen - DO NO EDIT MANUALLY + +from .core import Core +from pydantic import TypeAdapter +from .types import Group, GroupGetParams + + +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 + + async def get(self, group_id: str, group_params: GroupGetParams) -> Group: + response = await self.core.invoke( + { + "invocation": { + "clientId": self.client_id, + "parameters": { + "name": "GroupsGet", + "parameters": { + "group_id": group_id, + "group_params": group_params.model_dump(by_alias=True), + }, + }, + } + } + ) + + response = TypeAdapter(Group).validate_json(response) + return response diff --git a/src/onepassword/items.py b/src/onepassword/items.py index 0093e00..c6b6901 100644 --- a/src/onepassword/items.py +++ b/src/onepassword/items.py @@ -1,11 +1,19 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import _invoke, _invoke_sync -from typing import Optional, List +from .core import Core +from typing import List from pydantic import TypeAdapter from .items_shares import ItemsShares from .items_files import ItemsFiles -from .types import Item, ItemCreateParams, ItemListFilter, ItemOverview +from .types import ( + Item, + ItemCreateParams, + ItemListFilter, + ItemOverview, + ItemsDeleteAllResponse, + ItemsGetAllResponse, + ItemsUpdateAllResponse, +) class Items: @@ -13,17 +21,17 @@ class Items: The Items API holds all operations the SDK client can perform on 1Password items. """ - def __init__(self, client_id): + def __init__(self, client_id, core: Core): self.client_id = client_id - self.shares = ItemsShares(client_id) - - self.files = ItemsFiles(client_id) + self.core = core + self.shares = ItemsShares(client_id, core) + self.files = ItemsFiles(client_id, core) async def create(self, params: ItemCreateParams) -> Item: """ Create a new item. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -38,11 +46,35 @@ async def create(self, params: ItemCreateParams) -> Item: response = TypeAdapter(Item).validate_json(response) return response + async def create_all( + self, vault_id: str, params: List[ItemCreateParams] + ) -> ItemsUpdateAllResponse: + """ + Create items in batch, within a single vault. + """ + response = await self.core.invoke( + { + "invocation": { + "clientId": self.client_id, + "parameters": { + "name": "ItemsCreateAll", + "parameters": { + "vault_id": vault_id, + "params": [o.model_dump(by_alias=True) for o in params], + }, + }, + } + } + ) + + response = TypeAdapter(ItemsUpdateAllResponse).validate_json(response) + return response + async def get(self, vault_id: str, item_id: str) -> Item: """ Get an item by vault and item ID """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -57,11 +89,30 @@ async def get(self, vault_id: str, item_id: str) -> Item: response = TypeAdapter(Item).validate_json(response) return response + async def get_all(self, vault_id: str, item_ids: List[str]) -> ItemsGetAllResponse: + """ + Get items by vault and their item IDs. + """ + response = await self.core.invoke( + { + "invocation": { + "clientId": self.client_id, + "parameters": { + "name": "ItemsGetAll", + "parameters": {"vault_id": vault_id, "item_ids": item_ids}, + }, + } + } + ) + + response = TypeAdapter(ItemsGetAllResponse).validate_json(response) + return response + async def put(self, item: Item) -> Item: """ Update an existing item. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -80,7 +131,7 @@ async def delete(self, vault_id: str, item_id: str) -> None: """ Delete an item. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -94,11 +145,32 @@ async def delete(self, vault_id: str, item_id: str) -> None: return None + async def delete_all( + self, vault_id: str, item_ids: List[str] + ) -> ItemsDeleteAllResponse: + """ + Delete items in batch, within a single vault. + """ + response = await self.core.invoke( + { + "invocation": { + "clientId": self.client_id, + "parameters": { + "name": "ItemsDeleteAll", + "parameters": {"vault_id": vault_id, "item_ids": item_ids}, + }, + } + } + ) + + response = TypeAdapter(ItemsDeleteAllResponse).validate_json(response) + return response + async def archive(self, vault_id: str, item_id: str) -> None: """ Archive an item. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -116,7 +188,7 @@ async def list(self, vault_id: str, *filters: ItemListFilter) -> List[ItemOvervi """ List items based on filters. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, diff --git a/src/onepassword/items_files.py b/src/onepassword/items_files.py index 28213c9..d8c56ff 100644 --- a/src/onepassword/items_files.py +++ b/src/onepassword/items_files.py @@ -1,20 +1,21 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import _invoke, _invoke_sync -from typing import Optional, List +from .core import Core +from typing import List from pydantic import TypeAdapter from .types import DocumentCreateParams, FileAttributes, FileCreateParams, Item class ItemsFiles: - def __init__(self, client_id): + def __init__(self, client_id, core: Core): self.client_id = client_id + self.core = core async def attach(self, item: Item, file_params: FileCreateParams) -> Item: """ Attach files to Items """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -36,7 +37,7 @@ async def read(self, vault_id: str, item_id: str, attr: FileAttributes) -> bytes """ Read file content from the Item """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -59,7 +60,7 @@ 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 _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -84,7 +85,7 @@ async def replace_document( """ Replace the document file within a document item """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, diff --git a/src/onepassword/items_shares.py b/src/onepassword/items_shares.py index 473292d..715630d 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 _invoke, _invoke_sync -from typing import Optional, List +from typing import List from pydantic import TypeAdapter from .types import Item, ItemShareAccountPolicy, ItemShareParams, ValidRecipient class ItemsShares: - def __init__(self, client_id): + def __init__(self, client_id, core): self.client_id = client_id + self.core = core async def get_account_policy( self, vault_id: str, item_id: str @@ -16,7 +16,7 @@ async def get_account_policy( """ Get the item sharing policy of your account. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -37,7 +37,7 @@ async def validate_recipients( """ Validate the recipients of an item sharing link. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -61,7 +61,7 @@ async def create( """ Create a new item sharing link. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, diff --git a/src/onepassword/lib/aarch64/libop_uniffi_core.dylib b/src/onepassword/lib/aarch64/libop_uniffi_core.dylib index aed0646..430b993 100755 Binary files a/src/onepassword/lib/aarch64/libop_uniffi_core.dylib and b/src/onepassword/lib/aarch64/libop_uniffi_core.dylib differ diff --git a/src/onepassword/lib/aarch64/libop_uniffi_core.so b/src/onepassword/lib/aarch64/libop_uniffi_core.so index bb1a6aa..7f14693 100755 Binary files a/src/onepassword/lib/aarch64/libop_uniffi_core.so and b/src/onepassword/lib/aarch64/libop_uniffi_core.so differ diff --git a/src/onepassword/lib/x86_64/libop_uniffi_core.dylib b/src/onepassword/lib/x86_64/libop_uniffi_core.dylib index 06a99e3..1062d0f 100755 Binary files a/src/onepassword/lib/x86_64/libop_uniffi_core.dylib and b/src/onepassword/lib/x86_64/libop_uniffi_core.dylib differ diff --git a/src/onepassword/lib/x86_64/libop_uniffi_core.so b/src/onepassword/lib/x86_64/libop_uniffi_core.so index 1680d0e..c9ec993 100755 Binary files a/src/onepassword/lib/x86_64/libop_uniffi_core.so and b/src/onepassword/lib/x86_64/libop_uniffi_core.so differ diff --git a/src/onepassword/lib/x86_64/op_uniffi_core.dll b/src/onepassword/lib/x86_64/op_uniffi_core.dll index 3d15fa3..e08cb01 100644 Binary files a/src/onepassword/lib/x86_64/op_uniffi_core.dll and b/src/onepassword/lib/x86_64/op_uniffi_core.dll differ diff --git a/src/onepassword/secrets.py b/src/onepassword/secrets.py index c5f77a2..ffbd7d5 100644 --- a/src/onepassword/secrets.py +++ b/src/onepassword/secrets.py @@ -1,7 +1,7 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import _invoke, _invoke_sync -from typing import Optional, List +from .core import Core, UniffiCore +from typing import List from pydantic import TypeAdapter from .types import GeneratePasswordResponse, PasswordRecipe, ResolveAllResponse @@ -12,14 +12,15 @@ class Secrets: Use secret reference URIs to securely load secrets from 1Password: op:///[/]/ """ - def __init__(self, client_id): + def __init__(self, client_id, core: Core): self.client_id = client_id + self.core = core async def resolve(self, secret_reference: str) -> str: """ Resolve returns the secret the provided secret reference points to. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -38,7 +39,7 @@ 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 _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, @@ -58,7 +59,7 @@ def validate_secret_reference(secret_reference: str) -> None: """ Validate the secret reference to ensure there are no syntax errors. """ - response = _invoke_sync( + response = UniffiCore().invoke_sync( { "invocation": { "parameters": { @@ -73,7 +74,7 @@ def validate_secret_reference(secret_reference: str) -> None: @staticmethod def generate_password(recipe: PasswordRecipe) -> GeneratePasswordResponse: - response = _invoke_sync( + response = UniffiCore().invoke_sync( { "invocation": { "parameters": { diff --git a/src/onepassword/types.py b/src/onepassword/types.py index 37d2e33..e0989e7 100644 --- a/src/onepassword/types.py +++ b/src/onepassword/types.py @@ -140,6 +140,114 @@ class GeneratePasswordResponse(BaseModel): """ +class GroupType(str, Enum): + OWNERS = "owners" + """ + The owners group, which gives the following permissions: + - Do everything the Admin group can do + - See every vault other than the personal vaults + - Change people's names + - See billing + - Change billing + - Make other people owners + - Delete a person + """ + ADMINISTRATORS = "administrators" + """ + The administrators group, which gives the following permissions: + - Perform recovery + - Create new vaults + - Invite new members + - See vault metadata, including the vault name and who has access. + - Make other people admins + """ + RECOVERY = "recovery" + """ + The recovery group. It contains recovery keysets, and is added to every vault to allow for recovery. + + No one is added to this. + """ + EXTERNALACCOUNTMANAGERS = "externalAccountManagers" + """ + The external account managers group or EAM is a mandatory group for managed accounts that has + same permissions as the owners. + """ + TEAMMEMBERS = "teamMembers" + """ + Members of a team that a user is on. + """ + USERDEFINED = "userDefined" + """ + A custom, user defined group. + """ + UNSUPPORTED = "unsupported" + """ + Support for new or renamed group types + """ + + +class GroupState(str, Enum): + ACTIVE = "active" + """ + This group is active + """ + DELETED = "deleted" + """ + This group has been deleted + """ + UNSUPPORTED = "unsupported" + """ + This group is in an unknown state + """ + + +class VaultAccessorType(str, Enum): + USER = "user" + GROUP = "group" + + +class VaultAccess(BaseModel): + """ + Represents the vault access information. + """ + + model_config = ConfigDict(populate_by_name=True) + + vault_uuid: str = Field(alias="vaultUuid") + """ + The vault's UUID. + """ + accessor_type: VaultAccessorType = Field(alias="accessorType") + """ + The vault's accessor type. + """ + accessor_uuid: str = Field(alias="accessorUuid") + """ + The vault's accessor UUID. + """ + permissions: int + """ + The permissions granted to this vault + """ + + +class Group(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + id: str + title: str + description: str + group_type: GroupType = Field(alias="groupType") + state: GroupState + vault_access: Optional[List[VaultAccess]] = Field(alias="vaultAccess", default=None) + + +class GroupGetParams(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + vault_permissions: Optional[bool] = Field(alias="vaultPermissions", default=None) + + class ItemCategory(str, Enum): LOGIN = "Login" SECURENOTE = "SecureNote" @@ -717,6 +825,147 @@ class ItemShareParams(BaseModel): """ +class Response(BaseModel, Generic[T, E]): + content: Optional[T] = Field(default=None) + error: Optional[E] = Field(default=None) + + +class ItemUpdateFailureReasonTypes(str, Enum): + ITEM_VALIDATION_ERROR = "itemValidationError" + ITEM_STATUS_PERMISSION_ERROR = "itemStatusPermissionError" + ITEM_STATUS_INCORRECT_ITEM_VERSION = "itemStatusIncorrectItemVersion" + ITEM_STATUS_FILE_NOT_FOUND = "itemStatusFileNotFound" + ITEM_STATUS_TOO_BIG = "itemStatusTooBig" + ITEM_NOT_FOUND = "itemNotFound" + INTERNAL = "internal" + + +class ItemUpdateFailureReasonItemValidationError(BaseModel): + """ + Item update operation failed due to bad user input. + """ + + type: Literal[ItemUpdateFailureReasonTypes.ITEM_VALIDATION_ERROR] = ( + ItemUpdateFailureReasonTypes.ITEM_VALIDATION_ERROR + ) + message: ErrorMessage + + +class ItemUpdateFailureReasonItemStatusPermissionError(BaseModel): + """ + Item update operation is forbidden, permission issue. Make sure you have the correct permissions to update items in this vault. + """ + + type: Literal[ItemUpdateFailureReasonTypes.ITEM_STATUS_PERMISSION_ERROR] = ( + ItemUpdateFailureReasonTypes.ITEM_STATUS_PERMISSION_ERROR + ) + + +class ItemUpdateFailureReasonItemStatusIncorrectItemVersion(BaseModel): + """ + Item update operation failed due to incorrect version. + """ + + type: Literal[ItemUpdateFailureReasonTypes.ITEM_STATUS_INCORRECT_ITEM_VERSION] = ( + ItemUpdateFailureReasonTypes.ITEM_STATUS_INCORRECT_ITEM_VERSION + ) + + +class ItemUpdateFailureReasonItemStatusFileNotFound(BaseModel): + """ + Item update operation failed because a file reference didn't match a known file. + """ + + type: Literal[ItemUpdateFailureReasonTypes.ITEM_STATUS_FILE_NOT_FOUND] = ( + ItemUpdateFailureReasonTypes.ITEM_STATUS_FILE_NOT_FOUND + ) + + +class ItemUpdateFailureReasonItemStatusTooBig(BaseModel): + """ + Item update request is too big to be sent to the server. + """ + + type: Literal[ItemUpdateFailureReasonTypes.ITEM_STATUS_TOO_BIG] = ( + ItemUpdateFailureReasonTypes.ITEM_STATUS_TOO_BIG + ) + + +class ItemUpdateFailureReasonItemNotFound(BaseModel): + """ + The item was not found + """ + + type: Literal[ItemUpdateFailureReasonTypes.ITEM_NOT_FOUND] = ( + ItemUpdateFailureReasonTypes.ITEM_NOT_FOUND + ) + + +class ItemUpdateFailureReasonInternal(BaseModel): + """ + Item update operation experienced an internal error. + """ + + type: Literal[ItemUpdateFailureReasonTypes.INTERNAL] = ( + ItemUpdateFailureReasonTypes.INTERNAL + ) + message: ErrorMessage + + +ItemUpdateFailureReason = Union[ + ItemUpdateFailureReasonItemValidationError, + ItemUpdateFailureReasonItemStatusPermissionError, + ItemUpdateFailureReasonItemStatusIncorrectItemVersion, + ItemUpdateFailureReasonItemStatusFileNotFound, + ItemUpdateFailureReasonItemStatusTooBig, + ItemUpdateFailureReasonItemNotFound, + ItemUpdateFailureReasonInternal, +] + + +class ItemsDeleteAllResponse(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + individual_responses: Dict[str, Response[None, ItemUpdateFailureReason]] = Field( + alias="individualResponses" + ) + + +class ItemsGetAllErrorTypes(str, Enum): + ITEM_NOT_FOUND = "itemNotFound" + INTERNAL = "internal" + + +class ItemsGetAllErrorItemNotFound(BaseModel): + type: Literal[ItemsGetAllErrorTypes.ITEM_NOT_FOUND] = ( + ItemsGetAllErrorTypes.ITEM_NOT_FOUND + ) + + +class ItemsGetAllErrorInternal(BaseModel): + type: Literal[ItemsGetAllErrorTypes.INTERNAL] = ItemsGetAllErrorTypes.INTERNAL + message: ErrorMessage + + +ItemsGetAllError = Union[ItemsGetAllErrorItemNotFound, ItemsGetAllErrorInternal] + + +class ItemsGetAllResponse(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + individual_responses: List[Response[Item, ItemsGetAllError]] = Field( + alias="individualResponses" + ) + + +class ItemsUpdateAllResponse(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + individual_responses: List[Response[Item, ItemUpdateFailureReason]] = Field( + alias="individualResponses" + ) + + class OtpFieldDetails(BaseModel): """ Additional attributes for OTP fields. @@ -734,11 +983,6 @@ class OtpFieldDetails(BaseModel): """ -class Response(BaseModel, Generic[T, E]): - content: Optional[T] = Field(default=None) - error: Optional[E] = Field(default=None) - - class ResolvedReference(BaseModel): model_config = ConfigDict(populate_by_name=True) @@ -971,20 +1215,110 @@ class SshKeyAttributes(BaseModel): """ +class VaultType(str, Enum): + """ + Represents the vault type. + """ + + PERSONAL = "personal" + EVERYONE = "everyone" + TRANSFER = "transfer" + USERCREATED = "userCreated" + UNSUPPORTED = "unsupported" + + +class Vault(BaseModel): + """ + Represents regular vault information together with the vault's access information. + """ + + model_config = ConfigDict(populate_by_name=True) + + id: str + """ + The vault's ID. + """ + title: str + """ + The vault's title. + """ + description: str + """ + The description of the vault. + """ + vault_type: VaultType = Field(alias="vaultType") + """ + The type of the vault. + """ + active_item_count: int = Field(alias="activeItemCount") + """ + The number of active items within the vault. + """ + content_version: int = Field(alias="contentVersion") + """ + The content version number of the vault. It gets incremented whenever the state of the vault's contents changes (e.g. items from within the vault get created or updated). + """ + attribute_version: int = Field(alias="attributeVersion") + """ + The attribute version number of the vault. It gets incremented whenever vault presentation information changes, such as its title or icon. + """ + access: Optional[List[VaultAccess]] = Field(default=None) + """ + The access information associated with the vault. + """ + + +class VaultGetParams(BaseModel): + """ + Represents the possible query parameters used for retrieving extra information about a vault. + """ + + accessors: Optional[bool] = Field(default=None) + """ + The vault's accessor params. + """ + + +class VaultListParams(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + decrypt_details: Optional[bool] = Field(alias="decryptDetails", default=None) + + class VaultOverview(BaseModel): """ - Represents a decrypted 1Password vault. + Holds information about a 1Password Vault. """ model_config = ConfigDict(populate_by_name=True) id: str """ - The vault's ID + The vault's ID. """ title: str """ - The vault's title + The vault's title. + """ + description: str + """ + The description of this vault. + """ + vault_type: VaultType = Field(alias="vaultType") + """ + The type of the vault. + """ + active_item_count: int = Field(alias="activeItemCount") + """ + The number of active items within the vault. + """ + content_version: int = Field(alias="contentVersion") + """ + The content version number of the vault. It gets incremented whenever the state of the vault's contents changes (e.g. items from within the vault get created or updated). + """ + attribute_version: int = Field(alias="attributeVersion") + """ + The attribute version number of the vault. It gets incremented whenever vault presentation information changes, such as its title or icon. """ created_at: Annotated[ datetime, diff --git a/src/onepassword/vaults.py b/src/onepassword/vaults.py index 2b066d5..09d6adf 100644 --- a/src/onepassword/vaults.py +++ b/src/onepassword/vaults.py @@ -1,9 +1,14 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import _invoke, _invoke_sync +from .core import Core from typing import Optional, List from pydantic import TypeAdapter -from .types import VaultOverview +from .types import ( + Vault, + VaultGetParams, + VaultListParams, + VaultOverview, +) class Vaults: @@ -11,21 +16,60 @@ class Vaults: The Vaults API holds all the operations the SDK client can perform on 1Password vaults. """ - def __init__(self, client_id): + def __init__(self, client_id, core: Core): self.client_id = client_id + self.core = core - async def list(self) -> List[VaultOverview]: + async def list(self, params: Optional[VaultListParams] = None) -> List[VaultOverview]: """ - List all vaults + List information about vaults that's configurable based on some input parameters. """ - response = await _invoke( + response = await self.core.invoke( { "invocation": { "clientId": self.client_id, - "parameters": {"name": "VaultsList", "parameters": {}}, + "parameters": { + "name": "VaultsList", + "parameters": {"params": params.model_dump(by_alias=True) if params else None}, + }, } } ) response = TypeAdapter(List[VaultOverview]).validate_json(response) return response + + async def get_overview(self, vault_uuid: str) -> VaultOverview: + response = await self.core.invoke( + { + "invocation": { + "clientId": self.client_id, + "parameters": { + "name": "VaultsGetOverview", + "parameters": {"vault_uuid": vault_uuid}, + }, + } + } + ) + + response = TypeAdapter(VaultOverview).validate_json(response) + return response + + async def get(self, vault_uuid: str, vault_params: VaultGetParams) -> Vault: + response = await self.core.invoke( + { + "invocation": { + "clientId": self.client_id, + "parameters": { + "name": "VaultsGet", + "parameters": { + "vault_uuid": vault_uuid, + "vault_params": vault_params.model_dump(by_alias=True), + }, + }, + } + } + ) + + response = TypeAdapter(Vault).validate_json(response) + return response