diff --git a/README.md b/README.md index 12196e8c..c5cb3fbf 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,9 @@ Operations: - [x] [Create items](https://developer.1password.com/docs/sdks/manage-items#create-an-item) - [x] [Update items](https://developer.1password.com/docs/sdks/manage-items#update-an-item) - [x] [Delete items](https://developer.1password.com/docs/sdks/manage-items#delete-an-item) +- [x] Archive items - [x] [List items](https://developer.1password.com/docs/sdks/list-vaults-items/) -- [x] Add & update tags on items +- [x] Share items - [x] Generate PIN, random and memorable passwords Field types: @@ -96,15 +97,18 @@ Field types: - [x] Passwords - [x] Concealed fields - [x] Text fields -- [ ] Notes -- [x] SSH private keys (partially supported: supported in resolving secret references, not yet supported in item create/get/update) -- [ ] SSH public keys, fingerprint and key type +- [x] Notes +- [x] SSH private keys, public keys, fingerprint and key type (partially supported: supported in resolving secret references, not yet supported in item create/get/update) - [x] One-time passwords - [x] URLs - [x] Websites (used to suggest and autofill logins) - [x] Phone numbers - [x] Credit card types - [x] Credit card numbers +- [x] Emails +- [x] References to other items +- [ ] Address +- [ ] Date / MM/YY - [ ] Files attachments and Document items ### Vault management diff --git a/example/example.py b/example/example.py index f3e703ed..528f78ab 100644 --- a/example/example.py +++ b/example/example.py @@ -165,11 +165,56 @@ async def main(): print(random_password) # [developer-docs.sdk.python.generate-random-password]-end + await share_item(client, created_item.vault_id, updated_item.id) + # [developer-docs.sdk.python.delete-item]-start # Delete a item from your vault. await client.items.delete(created_item.vault_id, updated_item.id) # [developer-docs.sdk.python.delete-item]-end +## NOTE: this is in a separate function to avoid creating a new item +## NOTE: just for the sake of archiving it. This is because the SDK +## NOTE: only works with active items, so archiving and then deleting +## NOTE: is not yet possible. +async def archive_item(client: Client, vault_id: str, item_id: str): + # [developer-docs.sdk.python.archive-item]-start + # Archive a item from your vault. + await client.items.archive(vault_id, item_id) + # [developer-docs.sdk.python.archive-item]-end + + +async def share_item(client: Client, vault_id: str, item_id: str): + # [developer-docs.sdk.python.item-share-get-item]-start + item = await client.items.get(vault_id, item_id) + print(item) + # [developer-docs.sdk.python.item-share-get-item]-end + + # [developer-docs.sdk.python.item-share-get-account-policy]-start + policy = await client.items.shares.get_account_policy(item.vault_id, item.id) + print(policy) + # [developer-docs.sdk.python.item-share-get-account-policy]-end + + # [developer-docs.sdk.python.item-share-validate-recipients]-start + valid_recipients = await client.items.shares.validate_recipients( + policy, ["agilebits.com"] + ) + + print(valid_recipients) + # [developer-docs.sdk.python.item-share-validate-recipients]-end + + # [developer-docs.sdk.python.item-share-create-share]-start + share_link = await client.items.shares.create( + item, + policy, + ItemShareParams( + recipients = valid_recipients, + expireAfter= ItemShareDuration.ONEHOUR, + oneTimeOnly= False, + ), + ) + + print(share_link) + # [developer-docs.sdk.python.item-share-create-share]-end if __name__ == "__main__": asyncio.run(main()) diff --git a/src/onepassword/__init__.py b/src/onepassword/__init__.py index a2554da1..a605b0a8 100644 --- a/src/onepassword/__init__.py +++ b/src/onepassword/__init__.py @@ -24,7 +24,11 @@ for name, obj in inspect.getmembers(sys.modules["onepassword.types"]): # Add all classes and instances of typing.Literal defined in types.py. if ( - inspect.isclass(obj) - and inspect.getmodule(obj) == sys.modules["onepassword.types"] - ) or type(eval(name)) == typing._LiteralGenericAlias: + ( + inspect.isclass(obj) + and inspect.getmodule(obj) == sys.modules["onepassword.types"] + ) + or isinstance(obj, int) + or type(obj) == typing._LiteralGenericAlias + ): __all__.append(name) diff --git a/src/onepassword/build_number.py b/src/onepassword/build_number.py index c4cd8167..18e1c96e 100644 --- a/src/onepassword/build_number.py +++ b/src/onepassword/build_number.py @@ -1 +1 @@ -SDK_BUILD_NUMBER = "0010501" +SDK_BUILD_NUMBER = "0010601" diff --git a/src/onepassword/client.py b/src/onepassword/client.py index 4b8bc28b..ae5377f5 100644 --- a/src/onepassword/client.py +++ b/src/onepassword/client.py @@ -1,5 +1,6 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY +from __future__ import annotations import weakref from .core import _init_client, _release_client from .defaults import new_default_config @@ -14,7 +15,9 @@ class Client: vaults: Vaults @classmethod - async def authenticate(cls, auth, integration_name, integration_version): + async def authenticate( + cls, auth: str, integration_name: str, integration_version: str + ) -> Client: config = new_default_config( auth=auth or "", integration_name=integration_name, diff --git a/src/onepassword/items.py b/src/onepassword/items.py index 8365f907..60112050 100644 --- a/src/onepassword/items.py +++ b/src/onepassword/items.py @@ -1,9 +1,10 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import _invoke, _invoke_sync -from json import loads +from .core import _invoke from .iterator import SDKIterator -from .types import Item, ItemOverview +from pydantic import TypeAdapter +from .items_shares import ItemsShares +from .types import Item, ItemCreateParams, ItemOverview class Items: @@ -13,10 +14,11 @@ class Items: def __init__(self, client_id): self.client_id = client_id + self.shares = ItemsShares(client_id) - async def create(self, params): + async def create(self, params: ItemCreateParams) -> Item: """ - Create a new item + Create a new item. """ response = await _invoke( { @@ -29,9 +31,11 @@ async def create(self, params): } } ) - return Item.model_validate_json(response) - async def get(self, vault_id, item_id): + response = TypeAdapter(Item).validate_json(response) + return response + + async def get(self, vault_id: str, item_id: str) -> Item: """ Get an item by vault and item ID """ @@ -46,9 +50,11 @@ async def get(self, vault_id, item_id): } } ) - return Item.model_validate_json(response) - async def put(self, item): + response = TypeAdapter(Item).validate_json(response) + return response + + async def put(self, item: Item) -> Item: """ Update an existing item. """ @@ -63,13 +69,15 @@ async def put(self, item): } } ) - return Item.model_validate_json(response) - async def delete(self, vault_id, item_id): + response = TypeAdapter(Item).validate_json(response) + return response + + async def delete(self, vault_id: str, item_id: str): """ Delete an item. """ - await _invoke( + response = await _invoke( { "invocation": { "clientId": self.client_id, @@ -81,7 +89,27 @@ async def delete(self, vault_id, item_id): } ) - async def list_all(self, vault_id): + return None + + async def archive(self, vault_id: str, item_id: str): + """ + Archive an item. + """ + response = await _invoke( + { + "invocation": { + "clientId": self.client_id, + "parameters": { + "name": "ItemsArchive", + "parameters": {"vault_id": vault_id, "item_id": item_id}, + }, + } + } + ) + + return None + + async def list_all(self, vault_id: str) -> SDKIterator[ItemOverview]: """ List all items """ @@ -97,8 +125,5 @@ async def list_all(self, vault_id): } ) - response_data = loads(response) - - objects = [ItemOverview.model_validate(data) for data in response_data] - - return SDKIterator(objects) + response = TypeAdapter(list[ItemOverview]).validate_json(response) + return SDKIterator(response) diff --git a/src/onepassword/items_shares.py b/src/onepassword/items_shares.py new file mode 100644 index 00000000..93939681 --- /dev/null +++ b/src/onepassword/items_shares.py @@ -0,0 +1,80 @@ +# Code generated by op-codegen - DO NO EDIT MANUALLY + +from .core import _invoke +from pydantic import TypeAdapter +from .types import Item, ItemShareAccountPolicy, ItemShareParams, ValidRecipient + + +class ItemsShares: + def __init__(self, client_id): + self.client_id = client_id + + async def get_account_policy( + self, vault_id: str, item_id: str + ) -> ItemShareAccountPolicy: + """ + Get the item sharing policy of your account. + """ + response = await _invoke( + { + "invocation": { + "clientId": self.client_id, + "parameters": { + "name": "ItemsSharesGetAccountPolicy", + "parameters": {"vault_id": vault_id, "item_id": item_id}, + }, + } + } + ) + + response = TypeAdapter(ItemShareAccountPolicy).validate_json(response) + return response + + async def validate_recipients( + self, policy: ItemShareAccountPolicy, recipients: list[str] + ) -> list[ValidRecipient]: + """ + Validate the recipients of an item sharing link. + """ + response = await _invoke( + { + "invocation": { + "clientId": self.client_id, + "parameters": { + "name": "ItemsSharesValidateRecipients", + "parameters": { + "policy": policy.model_dump(by_alias=True), + "recipients": recipients, + }, + }, + } + } + ) + + response = TypeAdapter(list[ValidRecipient]).validate_json(response) + return response + + async def create( + self, item: Item, policy: ItemShareAccountPolicy, params: ItemShareParams + ) -> str: + """ + Create a new item sharing link. + """ + response = await _invoke( + { + "invocation": { + "clientId": self.client_id, + "parameters": { + "name": "ItemsSharesCreate", + "parameters": { + "item": item.model_dump(by_alias=True), + "policy": policy.model_dump(by_alias=True), + "params": params.model_dump(by_alias=True), + }, + }, + } + } + ) + + response = TypeAdapter(str).validate_json(response) + return response diff --git a/src/onepassword/lib/aarch64/libop_uniffi_core.dylib b/src/onepassword/lib/aarch64/libop_uniffi_core.dylib index 3dc273d3..3c68bd12 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 367490e4..e62007d8 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 25b7a5f3..45f17654 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 9414afb0..c663906d 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 56a58b68..c7fe3786 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 50503bd7..dc0bb6a3 100644 --- a/src/onepassword/secrets.py +++ b/src/onepassword/secrets.py @@ -1,9 +1,8 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY from .core import _invoke, _invoke_sync -from json import loads -from .iterator import SDKIterator -from .types import GeneratePasswordResponse +from pydantic import TypeAdapter +from .types import GeneratePasswordResponse, PasswordRecipe class Secrets: @@ -15,7 +14,7 @@ class Secrets: def __init__(self, client_id): self.client_id = client_id - async def resolve(self, secret_reference): + async def resolve(self, secret_reference: str) -> str: """ Resolve returns the secret the provided secret reference points to. """ @@ -30,14 +29,16 @@ async def resolve(self, secret_reference): } } ) - return str(loads(response)) + + response = TypeAdapter(str).validate_json(response) + return response @staticmethod - def validate_secret_reference(secret_reference): + def validate_secret_reference(secret_reference: str): """ Validate the secret reference to ensure there are no syntax errors. """ - _invoke_sync( + response = _invoke_sync( { "invocation": { "parameters": { @@ -48,8 +49,10 @@ def validate_secret_reference(secret_reference): } ) + return None + @staticmethod - def generate_password(recipe): + def generate_password(recipe: PasswordRecipe) -> GeneratePasswordResponse: response = _invoke_sync( { "invocation": { @@ -60,4 +63,6 @@ def generate_password(recipe): } } ) - return GeneratePasswordResponse.model_validate_json(response) + + response = TypeAdapter(GeneratePasswordResponse).validate_json(response) + return response diff --git a/src/onepassword/types.py b/src/onepassword/types.py index a0d10380..66dce22f 100644 --- a/src/onepassword/types.py +++ b/src/onepassword/types.py @@ -1,5 +1,5 @@ """ -Generated by typeshare 1.12.0 +Generated by typeshare 1.13.2 """ from __future__ import annotations @@ -56,6 +56,8 @@ class ItemFieldType(str, Enum): PHONE = "Phone" URL = "Url" TOTP = "Totp" + EMAIL = "Email" + REFERENCE = "Reference" UNSUPPORTED = "Unsupported" @@ -125,9 +127,25 @@ class ItemSection(BaseModel): class AutofillBehavior(str, Enum): + """ + Controls the auto-fill behavior of a website. + + + For more information, visit https://support.1password.com/autofill-behavior/ + """ + ANYWHEREONWEBSITE = "AnywhereOnWebsite" + """ + Auto-fill any page that’s part of the website, including subdomains + """ EXACTDOMAIN = "ExactDomain" + """ + Auto-fill only if the domain (hostname and port) is an exact match. + """ NEVER = "Never" + """ + Never auto-fill on this website + """ class Website(BaseModel): @@ -180,6 +198,10 @@ class Item(BaseModel): """ The item's sections """ + notes: str + """ + The notes of the item + """ tags: List[str] """ The item's tags @@ -217,6 +239,10 @@ class ItemCreateParams(BaseModel): """ The item's sections """ + notes: Optional[str] = Field(default=None) + """ + The item's notes + """ tags: Optional[List[str]] = Field(default=None) """ The item's tags @@ -256,6 +282,160 @@ class ItemOverview(BaseModel): """ +class ItemShareDuration(str, Enum): + """ + The valid duration options for sharing an item + """ + + ONEHOUR = "OneHour" + """ + The share will expire in one hour + """ + ONEDAY = "OneDay" + """ + The share will expire in one day + """ + SEVENDAYS = "SevenDays" + """ + The share will expire in seven days + """ + FOURTEENDAYS = "FourteenDays" + """ + The share will expire in fourteen days + """ + THIRTYDAYS = "ThirtyDays" + """ + The share will expire in thirty days + """ + + +class AllowedType(str, Enum): + """ + The allowed types of item sharing, enforced by account policy + """ + + AUTHENTICATED = "Authenticated" + """ + Allows creating share links with specific recipients + """ + PUBLIC = "Public" + """ + Allows creating public share links + """ + + +class AllowedRecipientType(str, Enum): + """ + The allowed recipient types of item sharing, enforced by account policy + """ + + EMAIL = "Email" + """ + Recipients can be specified by email address + """ + DOMAIN = "Domain" + """ + Recipients can be specified by domain + """ + + +class ItemShareAccountPolicy(BaseModel): + """ + The account policy for sharing items, set by your account owner/admin + This policy is enforced server-side when sharing items + """ + + model_config = ConfigDict(populate_by_name=True) + + max_expiry: ItemShareDuration = Field(alias="maxExpiry") + """ + The maximum duration that an item can be shared for + """ + default_expiry: ItemShareDuration = Field(alias="defaultExpiry") + """ + The default duration that an item is shared for + """ + max_views: Optional[int] = Field(alias="maxViews", default=None) + """ + The maximum number of times an item can be viewed. A null value means unlimited views + """ + allowed_types: List[AllowedType] = Field(alias="allowedTypes") + """ + The allowed types of item sharing - either "Authenticated" (share to specific users) or "Public" (share to anyone with a link) + """ + allowed_recipient_types: List[AllowedRecipientType] = Field( + alias="allowedRecipientTypes" + ) + """ + The allowed recipient types of item sharing - either "Email" or "Domain" + """ + + +class ValidRecipientEmailInner(BaseModel): + """ + Generated type representing the anonymous struct variant `Email` of the `ValidRecipient` Rust enum + """ + + email: str + + +class ValidRecipientDomainInner(BaseModel): + """ + Generated type representing the anonymous struct variant `Domain` of the `ValidRecipient` Rust enum + """ + + domain: str + + +class ValidRecipientTypes(str, Enum): + EMAIL = "Email" + DOMAIN = "Domain" + + +class ValidRecipientEmail(BaseModel): + """ + This exact email address + """ + + type: Literal[ValidRecipientTypes.EMAIL] = ValidRecipientTypes.EMAIL + parameters: ValidRecipientEmailInner + + +class ValidRecipientDomain(BaseModel): + """ + Anyone with an email address from the specified domain + """ + + type: Literal[ValidRecipientTypes.DOMAIN] = ValidRecipientTypes.DOMAIN + parameters: ValidRecipientDomainInner + + +# The validated recipient of an item share +ValidRecipient = Union[ValidRecipientEmail, ValidRecipientDomain] + + +class ItemShareParams(BaseModel): + """ + The configuration options for sharing an item + These must respect the account policy on item sharing + """ + + model_config = ConfigDict(populate_by_name=True) + + recipients: Optional[List[ValidRecipient]] = Field(default=None) + """ + Emails or domains of the item share recipients. If not provided, everyone with the share link will have access + """ + expire_after: Optional[ItemShareDuration] = Field(alias="expireAfter", default=None) + """ + The duration of the share in seconds. If not provided, defaults to the account policy's default expiry + """ + one_time_only: bool = Field(alias="oneTimeOnly") + """ + Whether the item can only be viewed once per recipient + """ + + class OtpFieldDetails(BaseModel): """ Additional attributes for OTP fields. @@ -371,15 +551,54 @@ class PasswordRecipeRandom(BaseModel): class SeparatorType(str, Enum): DIGITS = "digits" + """ + Randomly selected digits. + E.g, "`correct4horse0battery1staple`" + """ DIGITSANDSYMBOLS = "digitsAndSymbols" + """ + Randomly selected digits and symbols. + This is useful to get word-based passwords to meet complexity requirements + E.g, "`correct4horse-battery1staple`" + """ SPACES = "spaces" + """ + Spaces, like the original Diceware. + Great for mobile keyboards, not so great when people can overhear you type the password. + E.g, "`correct horse battery staple`" + """ HYPHENS = "hyphens" + """ + Hyphens "`-`". + E.g, "`correct-horse-battery-staple`" + """ UNDERSCORES = "underscores" + """ + "`_`". + E.g, "`correct_horse_battery_staple`" + """ PERIODS = "periods" + """ + Period (full stop) "`.`". + E.g, "`correct.horse.battery.staple`" + """ COMMAS = "commas" + """ + Comma "`,`". + E.g, "`correct,horse,battery,staple`" + """ class WordListType(str, Enum): FULLWORDS = "fullWords" + """ + Agile wordlist + """ SYLLABLES = "syllables" + """ + English-like syllables + """ THREELETTERS = "threeLetters" + """ + Three (random) letter "words" + """ diff --git a/src/onepassword/vaults.py b/src/onepassword/vaults.py index 5dbee83b..84e7555e 100644 --- a/src/onepassword/vaults.py +++ b/src/onepassword/vaults.py @@ -1,8 +1,8 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import _invoke, _invoke_sync -from json import loads +from .core import _invoke from .iterator import SDKIterator +from pydantic import TypeAdapter from .types import VaultOverview @@ -14,7 +14,7 @@ class Vaults: def __init__(self, client_id): self.client_id = client_id - async def list_all(self): + async def list_all(self) -> SDKIterator[VaultOverview]: """ List all vaults """ @@ -27,8 +27,5 @@ async def list_all(self): } ) - response_data = loads(response) - - objects = [VaultOverview.model_validate(data) for data in response_data] - - return SDKIterator(objects) + response = TypeAdapter(list[VaultOverview]).validate_json(response) + return SDKIterator(response) diff --git a/src/release/RELEASE-NOTES b/src/release/RELEASE-NOTES index 5780b4d9..fc65bead 100644 --- a/src/release/RELEASE-NOTES +++ b/src/release/RELEASE-NOTES @@ -1,4 +1,15 @@ -Version 0.1.5 of the 1Password Python SDK brings: -* Support for generating passwords. You can now generate random, PIN, and memorable passwords using the `onepassword.Secrets.generate_password` function. -* Support for item subtitles. Creating and editing an item now sets the subtitle correctly, which is visible in the item preview in all client apps. -* Support for the Credit Card Number field type. You can now retrieve, create, and edit items containing credit card numbers. +# 1Password Python SDK v0.1.6 + +## NEW +* Support for item sharing: You can now create an item sharing link via the 1Password SDKs using the new `client.items.shares` API. +* Support for item archiving: You can now move items to the archive with the SDKs, using the new `client.items.archive(vault_uuid, item_uuid)` function. + +## IMPROVED +* Support for item notes: You can now read, create and edit items with a notes field, accessing it via `item.notes`. +* Support for SSH key attributes in secret references: You can now retrieve an SSH key's public key, key type and fingerprint with `client.secrets.resolve`. +* Support for additional field types: You can now read, create and edit items with Email (email addresses) and Reference (ID references to other 1Password items) type fields. +* Type hinting: When developing with the Python SDK, the functions now show type hints on their parameters and return types. + +## BUGS +* Improved field matching logic for secret references: Retrieving a field from the item's default field section is now possible even if there is an identically named field in one of the item's named sections. + diff --git a/version.py b/version.py index 18df7134..de2b61b7 100644 --- a/version.py +++ b/version.py @@ -1 +1 @@ -SDK_VERSION = "0.1.5" +SDK_VERSION = "0.1.6"