diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ff97ad..634a135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 24.3.0 + +* Added: `account` passkey methods `listPasskeys`, `createPasskey`, `getPasskey`, `updatePasskey`, `deletePasskey` and `updatePasskeyVerification` +* Added: `account.createPasskeyToken` and `account.updatePasskeyToken` for passkey sign-in +* Added: `users.listPasskeys`, `users.getPasskey`, `users.updatePasskey` and `users.deletePasskey` +* Added: `project.updatePasskeyPolicy` to configure passkey sign-in +* Added: `Passkey`, `PasskeyList`, `PasskeyChallenge` and `TokenSecret` models + ## 24.2.0 * Added: optional `interval` parameter on `functions.create` and `functions.update` to run a function every N minutes diff --git a/appwrite/client.py b/appwrite/client.py index 470508a..f1c96d6 100644 --- a/appwrite/client.py +++ b/appwrite/client.py @@ -18,11 +18,11 @@ def __init__(self): self._endpoint = 'https://cloud.appwrite.io/v1' self._global_headers = { 'content-type': '', - 'user-agent': f'AppwritePythonSDK/24.2.0 ({platform.uname().system}; {platform.uname().version}; {platform.uname().machine})', + 'user-agent': f'AppwritePythonSDK/24.3.0 ({platform.uname().system}; {platform.uname().version}; {platform.uname().machine})', 'x-sdk-name': 'Python', 'x-sdk-platform': 'server', 'x-sdk-language': 'python', - 'x-sdk-version': '24.2.0', + 'x-sdk-version': '24.3.0', 'X-Appwrite-Response-Format': '2.0.0', } self._config = {} diff --git a/appwrite/models/__init__.py b/appwrite/models/__init__.py index 80ced38..5acdce0 100644 --- a/appwrite/models/__init__.py +++ b/appwrite/models/__init__.py @@ -208,6 +208,10 @@ from .email_template import EmailTemplate from .mfa_challenge import MfaChallenge from .mfa_challenge_secret import MfaChallengeSecret +from .token_secret import TokenSecret +from .passkey import Passkey +from .passkey_challenge import PasskeyChallenge +from .passkey_list import PasskeyList from .mfa_recovery_codes import MfaRecoveryCodes from .mfa_type import MfaType from .mfa_factors import MfaFactors @@ -511,6 +515,10 @@ 'EmailTemplate', 'MfaChallenge', 'MfaChallengeSecret', + 'TokenSecret', + 'Passkey', + 'PasskeyChallenge', + 'PasskeyList', 'MfaRecoveryCodes', 'MfaType', 'MfaFactors', diff --git a/appwrite/models/passkey.py b/appwrite/models/passkey.py new file mode 100644 index 0000000..556095a --- /dev/null +++ b/appwrite/models/passkey.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel + + +class Passkey(AppwriteModel): + """ + Passkey + + Attributes + ---------- + id : str + Passkey ID. + createdat : str + Passkey creation date in ISO 8601 format. + updatedat : str + Passkey update date in ISO 8601 format. + name : str + Passkey name. + accessedat : str + Most recent sign-in with this passkey in ISO 8601 format. Empty until the passkey is used to sign in. + """ + + id: str = Field(..., alias='$id') + createdat: str = Field(..., alias='$createdAt') + updatedat: str = Field(..., alias='$updatedAt') + name: str = Field(..., alias='name') + accessedat: str = Field(..., alias='accessedAt') diff --git a/appwrite/models/passkey_challenge.py b/appwrite/models/passkey_challenge.py new file mode 100644 index 0000000..6f40851 --- /dev/null +++ b/appwrite/models/passkey_challenge.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel + + +class PasskeyChallenge(AppwriteModel): + """ + Passkey Challenge + + Attributes + ---------- + id : str + Challenge ID. When signing in, pass it back with the credential to receive a token. + createdat : str + Challenge creation date in ISO 8601 format. + passkeyid : str + ID of the passkey being registered. Empty for sign-in challenges. + expire : str + Challenge expiration date in ISO 8601 format. + publickey : Dict[str, Any] + WebAuthn options in JSON form. Pass them to `PublicKeyCredential.parseCreationOptionsFromJSON()` when registering, or `PublicKeyCredential.parseRequestOptionsFromJSON()` when signing in. + """ + + id: str = Field(..., alias='$id') + createdat: str = Field(..., alias='$createdAt') + passkeyid: str = Field(..., alias='passkeyId') + expire: str = Field(..., alias='expire') + publickey: Dict[str, Any] = Field(..., alias='publicKey') diff --git a/appwrite/models/passkey_list.py b/appwrite/models/passkey_list.py new file mode 100644 index 0000000..b492b3f --- /dev/null +++ b/appwrite/models/passkey_list.py @@ -0,0 +1,21 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel +from .passkey import Passkey + + +class PasskeyList(AppwriteModel): + """ + Passkeys List + + Attributes + ---------- + total : float + Total number of passkeys that matched your query. + passkeys : List[Passkey] + List of passkeys. + """ + + total: float = Field(..., alias='total') + passkeys: List[Passkey] = Field(..., alias='passkeys') diff --git a/appwrite/models/token_secret.py b/appwrite/models/token_secret.py new file mode 100644 index 0000000..6d89eed --- /dev/null +++ b/appwrite/models/token_secret.py @@ -0,0 +1,32 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel + + +class TokenSecret(AppwriteModel): + """ + Token Secret + + Attributes + ---------- + id : str + Token ID. + createdat : str + Token creation date in ISO 8601 format. + userid : str + User ID. + secret : str + Token secret key. Exchange it for a session with the create session endpoint. + expire : str + Token expiration date in ISO 8601 format. + phrase : str + Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email. + """ + + id: str = Field(..., alias='$id') + createdat: str = Field(..., alias='$createdAt') + userid: str = Field(..., alias='userId') + secret: str = Field(..., alias='secret') + expire: str = Field(..., alias='expire') + phrase: str = Field(..., alias='phrase') diff --git a/appwrite/services/account.py b/appwrite/services/account.py index 03c7bc3..6520053 100644 --- a/appwrite/services/account.py +++ b/appwrite/services/account.py @@ -17,10 +17,14 @@ from ..models.session import Session from ..models.mfa_factors import MfaFactors from ..models.mfa_recovery_codes import MfaRecoveryCodes +from ..models.passkey_list import PasskeyList +from ..models.passkey_challenge import PasskeyChallenge +from ..models.passkey import Passkey from ..models.preferences import Preferences from ..models.token import Token from ..models.session_list import SessionList from ..enums.o_auth_provider import OAuthProvider +from ..models.token_secret import TokenSecret T = TypeVar('T') @@ -1014,6 +1018,270 @@ def update_name( return User.with_data(response, model_type) + def list_passkeys( + self, + queries: Optional[List[str]] = None, + total: Optional[bool] = None, + ) -> PasskeyList: + """ + Get the list of verified passkeys registered by the currently logged in user. + + Parameters + ---------- + queries : Optional[List[str]] + Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter and order on the following attributes: $id, $createdAt, $updatedAt, name, accessedAt + total : Optional[bool] + When set to false, the total count returned will be 0 and will not be calculated. + Returns + ------- + PasskeyList + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/passkeys' + api_params = {} + if queries is not None: + api_params['queries'] = self._normalize_value(queries) + if total is not None: + api_params['total'] = self._normalize_value(total) + + response = self.client.call( + 'get', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'accept': 'application/json', + }, + api_params, + ) + + return self._parse_response(response, model=PasskeyList) + + def create_passkey( + self, + passkey_id: Optional[str] = None, + name: Optional[str] = None, + ) -> PasskeyChallenge: + """ + Start registering a passkey for the currently logged in user. The session must have signed in or completed an MFA challenge within the last 10 minutes. Pass the returned `publicKey` options to `navigator.credentials.create()`, then complete the registration with [Update passkey verification](/docs/references/cloud/client-web/account#updatePasskeyVerification). The passkey stays pending until verified, and the challenge expires after 5 minutes. + + Parameters + ---------- + passkey_id : Optional[str] + Passkey ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. + name : Optional[str] + Passkey name, shown when listing passkeys. Max length: 128 chars. + Returns + ------- + PasskeyChallenge + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/passkeys' + api_params = {} + if passkey_id is not None: + api_params['passkeyId'] = self._normalize_value(passkey_id) + if name is not None: + api_params['name'] = self._normalize_value(name) + + response = self.client.call( + 'post', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'content-type': 'application/json', + 'accept': 'application/json', + }, + api_params, + ) + + return self._parse_response(response, model=PasskeyChallenge) + + def get_passkey( + self, + passkey_id: str, + ) -> Passkey: + """ + Get a verified passkey of the currently logged in user by its unique ID. + + Parameters + ---------- + passkey_id : str + Passkey ID. + Returns + ------- + Passkey + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/passkeys/{passkeyId}' + api_params = {} + if passkey_id is None or passkey_id == '': + raise AppwriteException('Missing required parameter: "passkey_id"') + api_path = api_path.replace('{passkeyId}', str(self._normalize_value(passkey_id))) + + response = self.client.call( + 'get', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'accept': 'application/json', + }, + api_params, + ) + + return self._parse_response(response, model=Passkey) + + def update_passkey( + self, + passkey_id: str, + name: str, + ) -> Passkey: + """ + Rename a passkey of the currently logged in user. + + Parameters + ---------- + passkey_id : str + Passkey ID. + name : str + Passkey name. Max length: 128 chars. + Returns + ------- + Passkey + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/passkeys/{passkeyId}' + api_params = {} + if passkey_id is None or passkey_id == '': + raise AppwriteException('Missing required parameter: "passkey_id"') + if name is None: + raise AppwriteException('Missing required parameter: "name"') + api_path = api_path.replace('{passkeyId}', str(self._normalize_value(passkey_id))) + api_params['name'] = self._normalize_value(name) + + response = self.client.call( + 'patch', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'content-type': 'application/json', + 'accept': 'application/json', + }, + api_params, + ) + + return self._parse_response(response, model=Passkey) + + def delete_passkey( + self, + passkey_id: str, + ) -> Dict[str, Any]: + """ + Delete a passkey from the currently logged in user. The passkey can no longer be used to sign in, although it may remain stored on the user's device. The session must have signed in or completed an MFA challenge within the last 10 minutes. + + Parameters + ---------- + passkey_id : str + Passkey ID. + Returns + ------- + Dict[str, Any] + API response as a dictionary + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/passkeys/{passkeyId}' + api_params = {} + if passkey_id is None or passkey_id == '': + raise AppwriteException('Missing required parameter: "passkey_id"') + api_path = api_path.replace('{passkeyId}', str(self._normalize_value(passkey_id))) + + response = self.client.call( + 'delete', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'content-type': 'application/json', + 'accept': 'application/json', + }, + api_params, + ) + + return response + + def update_passkey_verification( + self, + passkey_id: str, + credential: Dict[str, Any], + ) -> Passkey: + """ + Complete a passkey registration started with [Create passkey](/docs/references/cloud/client-web/account#createPasskey). Pass the JSON form of the credential returned by `navigator.credentials.create()`, for example `credential.toJSON()`. Each registration can be verified once; a failed attempt requires starting it again. + + Parameters + ---------- + passkey_id : str + Passkey ID. + credential : Dict[str, Any] + Registration credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + Returns + ------- + Passkey + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/passkeys/{passkeyId}/verification' + api_params = {} + if passkey_id is None or passkey_id == '': + raise AppwriteException('Missing required parameter: "passkey_id"') + if credential is None: + raise AppwriteException('Missing required parameter: "credential"') + api_path = api_path.replace('{passkeyId}', str(self._normalize_value(passkey_id))) + api_params['credential'] = self._normalize_value(credential) + + response = self.client.call( + 'put', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'content-type': 'application/json', + 'accept': 'application/json', + }, + api_params, + ) + + return self._parse_response(response, model=Passkey) + def update_password( self, password: str, @@ -1928,6 +2196,85 @@ def create_o_auth2_token( return response + def create_passkey_token( + self, + ) -> PasskeyChallenge: + """ + Start signing in with a passkey. No email or user ID is needed: pass the returned `publicKey` options to `navigator.credentials.get()` and the user picks one of their passkeys. Then call [Update passkey token](/docs/references/cloud/client-web/account#updatePasskeyToken) with the credential to receive a token, and exchange it with [Create session](/docs/references/cloud/client-web/account#createSession). The challenge expires after 5 minutes. + Returns + ------- + PasskeyChallenge + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/tokens/passkey' + api_params = {} + + response = self.client.call( + 'post', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'content-type': 'application/json', + 'accept': 'application/json', + }, + api_params, + ) + + return self._parse_response(response, model=PasskeyChallenge) + + def update_passkey_token( + self, + challenge_id: str, + credential: Dict[str, Any], + ) -> TokenSecret: + """ + Complete a passkey sign-in started with [Create passkey token](/docs/references/cloud/client-web/account#createPasskeyToken). Pass the challenge ID and the JSON form of the credential returned by `navigator.credentials.get()`, for example `credential.toJSON()`. The returned token includes its secret and expires after 1 minute: exchange it for a session with [Create session](/docs/references/cloud/client-web/account#createSession). Passkeys require user verification, so the session also satisfies MFA. Each challenge can only be used once. + + Parameters + ---------- + challenge_id : str + Challenge ID returned by createPasskeyToken. + credential : Dict[str, Any] + Authentication credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + Returns + ------- + TokenSecret + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/tokens/passkey' + api_params = {} + if challenge_id is None: + raise AppwriteException('Missing required parameter: "challenge_id"') + if credential is None: + raise AppwriteException('Missing required parameter: "credential"') + api_params['challengeId'] = self._normalize_value(challenge_id) + api_params['credential'] = self._normalize_value(credential) + + response = self.client.call( + 'put', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'content-type': 'application/json', + 'accept': 'application/json', + }, + api_params, + ) + + return self._parse_response(response, model=TokenSecret) + def create_phone_token( self, user_id: str, diff --git a/appwrite/services/project.py b/appwrite/services/project.py index 848e448..59a898c 100644 --- a/appwrite/services/project.py +++ b/appwrite/services/project.py @@ -4467,6 +4467,47 @@ def update_mfa_factors_policy( return self._parse_response(response, model=ProjectModel) + def update_passkey_policy( + self, + rp_id: str, + ) -> ProjectModel: + """ + Set the relying party ID passkeys are bound to: the domain of your application. Passkeys work on the project's web platforms on that domain or its subdomains, and on the domain itself for Apple apps. A localhost web platform enables passkeys for local development without a relying party ID. The relying party ID cannot change while users have passkeys, and any change invalidates ceremonies in progress. + + Parameters + ---------- + rp_id : str + Relying party ID: the domain of your application, such as `example.com`. Use `localhost` for local development, or an empty string to unset it. + Returns + ------- + ProjectModel + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/project/policies/passkey' + api_params = {} + if rp_id is None: + raise AppwriteException('Missing required parameter: "rp_id"') + api_params['rpId'] = self._normalize_value(rp_id) + + response = self.client.call( + 'patch', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'content-type': 'application/json', + 'accept': 'application/json', + }, + api_params, + ) + + return self._parse_response(response, model=ProjectModel) + def update_password_dictionary_policy( self, enabled: bool, diff --git a/appwrite/services/users.py b/appwrite/services/users.py index 1c7ffc6..a2fb834 100644 --- a/appwrite/services/users.py +++ b/appwrite/services/users.py @@ -14,6 +14,8 @@ from ..models.mfa_challenge_secret import MfaChallengeSecret from ..models.mfa_factors import MfaFactors from ..models.mfa_recovery_codes import MfaRecoveryCodes +from ..models.passkey_list import PasskeyList +from ..models.passkey import Passkey from ..models.preferences import Preferences from ..models.session_list import SessionList from ..models.session import Session @@ -1475,6 +1477,202 @@ def update_name( return User.with_data(response, model_type) + def list_passkeys( + self, + user_id: str, + queries: Optional[List[str]] = None, + total: Optional[bool] = None, + ) -> PasskeyList: + """ + Get the list of verified passkeys registered by a user. + + Parameters + ---------- + user_id : str + User ID. + queries : Optional[List[str]] + Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter and order on the following attributes: $id, $createdAt, $updatedAt, name, accessedAt + total : Optional[bool] + When set to false, the total count returned will be 0 and will not be calculated. + Returns + ------- + PasskeyList + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/users/{userId}/passkeys' + api_params = {} + if user_id is None or user_id == '': + raise AppwriteException('Missing required parameter: "user_id"') + api_path = api_path.replace('{userId}', str(self._normalize_value(user_id))) + if queries is not None: + api_params['queries'] = self._normalize_value(queries) + if total is not None: + api_params['total'] = self._normalize_value(total) + + response = self.client.call( + 'get', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'accept': 'application/json', + }, + api_params, + ) + + return self._parse_response(response, model=PasskeyList) + + def get_passkey( + self, + user_id: str, + passkey_id: str, + ) -> Passkey: + """ + Get a verified passkey of a user by its unique ID. + + Parameters + ---------- + user_id : str + User ID. + passkey_id : str + Passkey ID. + Returns + ------- + Passkey + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/users/{userId}/passkeys/{passkeyId}' + api_params = {} + if user_id is None or user_id == '': + raise AppwriteException('Missing required parameter: "user_id"') + if passkey_id is None or passkey_id == '': + raise AppwriteException('Missing required parameter: "passkey_id"') + api_path = api_path.replace('{userId}', str(self._normalize_value(user_id))) + api_path = api_path.replace('{passkeyId}', str(self._normalize_value(passkey_id))) + + response = self.client.call( + 'get', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'accept': 'application/json', + }, + api_params, + ) + + return self._parse_response(response, model=Passkey) + + def update_passkey( + self, + user_id: str, + passkey_id: str, + name: str, + ) -> Passkey: + """ + Rename a passkey of a user. + + Parameters + ---------- + user_id : str + User ID. + passkey_id : str + Passkey ID. + name : str + Passkey name. Max length: 128 chars. + Returns + ------- + Passkey + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/users/{userId}/passkeys/{passkeyId}' + api_params = {} + if user_id is None or user_id == '': + raise AppwriteException('Missing required parameter: "user_id"') + if passkey_id is None or passkey_id == '': + raise AppwriteException('Missing required parameter: "passkey_id"') + if name is None: + raise AppwriteException('Missing required parameter: "name"') + api_path = api_path.replace('{userId}', str(self._normalize_value(user_id))) + api_path = api_path.replace('{passkeyId}', str(self._normalize_value(passkey_id))) + api_params['name'] = self._normalize_value(name) + + response = self.client.call( + 'patch', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'content-type': 'application/json', + 'accept': 'application/json', + }, + api_params, + ) + + return self._parse_response(response, model=Passkey) + + def delete_passkey( + self, + user_id: str, + passkey_id: str, + ) -> Dict[str, Any]: + """ + Delete a passkey from a user, for example when a device is lost. The passkey can no longer be used to sign in. + + Parameters + ---------- + user_id : str + User ID. + passkey_id : str + Passkey ID. + Returns + ------- + Dict[str, Any] + API response as a dictionary + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/users/{userId}/passkeys/{passkeyId}' + api_params = {} + if user_id is None or user_id == '': + raise AppwriteException('Missing required parameter: "user_id"') + if passkey_id is None or passkey_id == '': + raise AppwriteException('Missing required parameter: "passkey_id"') + api_path = api_path.replace('{userId}', str(self._normalize_value(user_id))) + api_path = api_path.replace('{passkeyId}', str(self._normalize_value(passkey_id))) + + response = self.client.call( + 'delete', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'content-type': 'application/json', + 'accept': 'application/json', + }, + api_params, + ) + + return response + def update_password( self, user_id: str, diff --git a/docs/examples/account/create-passkey-token.md b/docs/examples/account/create-passkey-token.md new file mode 100644 index 0000000..5e33ab5 --- /dev/null +++ b/docs/examples/account/create-passkey-token.md @@ -0,0 +1,16 @@ +```python +from appwrite.client import Client +from appwrite.services.account import Account +from appwrite.models import PasskeyChallenge + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_session('') # The user session to authenticate with + +account = Account(client) + +result: PasskeyChallenge = account.create_passkey_token() + +print(result.model_dump()) +``` diff --git a/docs/examples/account/create-passkey.md b/docs/examples/account/create-passkey.md new file mode 100644 index 0000000..75bc56f --- /dev/null +++ b/docs/examples/account/create-passkey.md @@ -0,0 +1,19 @@ +```python +from appwrite.client import Client +from appwrite.services.account import Account +from appwrite.models import PasskeyChallenge + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_session('') # The user session to authenticate with + +account = Account(client) + +result: PasskeyChallenge = account.create_passkey( + passkey_id = '', # optional + name = '' # optional +) + +print(result.model_dump()) +``` diff --git a/docs/examples/account/delete-passkey.md b/docs/examples/account/delete-passkey.md new file mode 100644 index 0000000..c366ed6 --- /dev/null +++ b/docs/examples/account/delete-passkey.md @@ -0,0 +1,15 @@ +```python +from appwrite.client import Client +from appwrite.services.account import Account + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_session('') # The user session to authenticate with + +account = Account(client) + +result = account.delete_passkey( + passkey_id = '' +) +``` diff --git a/docs/examples/account/get-passkey.md b/docs/examples/account/get-passkey.md new file mode 100644 index 0000000..018f846 --- /dev/null +++ b/docs/examples/account/get-passkey.md @@ -0,0 +1,18 @@ +```python +from appwrite.client import Client +from appwrite.services.account import Account +from appwrite.models import Passkey + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_session('') # The user session to authenticate with + +account = Account(client) + +result: Passkey = account.get_passkey( + passkey_id = '' +) + +print(result.model_dump()) +``` diff --git a/docs/examples/account/list-passkeys.md b/docs/examples/account/list-passkeys.md new file mode 100644 index 0000000..8c8df02 --- /dev/null +++ b/docs/examples/account/list-passkeys.md @@ -0,0 +1,19 @@ +```python +from appwrite.client import Client +from appwrite.services.account import Account +from appwrite.models import PasskeyList + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_session('') # The user session to authenticate with + +account = Account(client) + +result: PasskeyList = account.list_passkeys( + queries = [], # optional + total = False # optional +) + +print(result.model_dump()) +``` diff --git a/docs/examples/account/update-passkey-token.md b/docs/examples/account/update-passkey-token.md new file mode 100644 index 0000000..21fabcb --- /dev/null +++ b/docs/examples/account/update-passkey-token.md @@ -0,0 +1,19 @@ +```python +from appwrite.client import Client +from appwrite.services.account import Account +from appwrite.models import TokenSecret + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_session('') # The user session to authenticate with + +account = Account(client) + +result: TokenSecret = account.update_passkey_token( + challenge_id = '', + credential = {} +) + +print(result.model_dump()) +``` diff --git a/docs/examples/account/update-passkey-verification.md b/docs/examples/account/update-passkey-verification.md new file mode 100644 index 0000000..6490420 --- /dev/null +++ b/docs/examples/account/update-passkey-verification.md @@ -0,0 +1,19 @@ +```python +from appwrite.client import Client +from appwrite.services.account import Account +from appwrite.models import Passkey + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_session('') # The user session to authenticate with + +account = Account(client) + +result: Passkey = account.update_passkey_verification( + passkey_id = '', + credential = {} +) + +print(result.model_dump()) +``` diff --git a/docs/examples/account/update-passkey.md b/docs/examples/account/update-passkey.md new file mode 100644 index 0000000..aae42a4 --- /dev/null +++ b/docs/examples/account/update-passkey.md @@ -0,0 +1,19 @@ +```python +from appwrite.client import Client +from appwrite.services.account import Account +from appwrite.models import Passkey + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_session('') # The user session to authenticate with + +account = Account(client) + +result: Passkey = account.update_passkey( + passkey_id = '', + name = '' +) + +print(result.model_dump()) +``` diff --git a/docs/examples/project/update-passkey-policy.md b/docs/examples/project/update-passkey-policy.md new file mode 100644 index 0000000..08e2f41 --- /dev/null +++ b/docs/examples/project/update-passkey-policy.md @@ -0,0 +1,18 @@ +```python +from appwrite.client import Client +from appwrite.services.project import Project +from appwrite.models import Project as ProjectModel + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_key('') # Your secret API key + +project = Project(client) + +result: ProjectModel = project.update_passkey_policy( + rp_id = '' +) + +print(result.model_dump()) +``` diff --git a/docs/examples/users/delete-passkey.md b/docs/examples/users/delete-passkey.md new file mode 100644 index 0000000..060b39d --- /dev/null +++ b/docs/examples/users/delete-passkey.md @@ -0,0 +1,16 @@ +```python +from appwrite.client import Client +from appwrite.services.users import Users + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_key('') # Your secret API key + +users = Users(client) + +result = users.delete_passkey( + user_id = '', + passkey_id = '' +) +``` diff --git a/docs/examples/users/get-passkey.md b/docs/examples/users/get-passkey.md new file mode 100644 index 0000000..c7194bb --- /dev/null +++ b/docs/examples/users/get-passkey.md @@ -0,0 +1,19 @@ +```python +from appwrite.client import Client +from appwrite.services.users import Users +from appwrite.models import Passkey + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_key('') # Your secret API key + +users = Users(client) + +result: Passkey = users.get_passkey( + user_id = '', + passkey_id = '' +) + +print(result.model_dump()) +``` diff --git a/docs/examples/users/list-passkeys.md b/docs/examples/users/list-passkeys.md new file mode 100644 index 0000000..8853711 --- /dev/null +++ b/docs/examples/users/list-passkeys.md @@ -0,0 +1,20 @@ +```python +from appwrite.client import Client +from appwrite.services.users import Users +from appwrite.models import PasskeyList + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_key('') # Your secret API key + +users = Users(client) + +result: PasskeyList = users.list_passkeys( + user_id = '', + queries = [], # optional + total = False # optional +) + +print(result.model_dump()) +``` diff --git a/docs/examples/users/update-passkey.md b/docs/examples/users/update-passkey.md new file mode 100644 index 0000000..e8a2bb1 --- /dev/null +++ b/docs/examples/users/update-passkey.md @@ -0,0 +1,20 @@ +```python +from appwrite.client import Client +from appwrite.services.users import Users +from appwrite.models import Passkey + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_key('') # Your secret API key + +users = Users(client) + +result: Passkey = users.update_passkey( + user_id = '', + passkey_id = '', + name = '' +) + +print(result.model_dump()) +``` diff --git a/pyproject.toml b/pyproject.toml index e8e2aa0..75b5183 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "appwrite" -version = "24.2.0" +version = "24.3.0" description = "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API" readme = "README.md" requires-python = ">=3.9" diff --git a/setup.py b/setup.py index eb5e4ea..ac9d806 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setuptools.setup( name='appwrite', packages=setuptools.find_packages(), - version='24.2.0', + version='24.3.0', license='BSD-3-Clause', description='Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API', long_description=long_description, @@ -18,7 +18,7 @@ maintainer='Appwrite Team', maintainer_email='team@appwrite.io', url='https://appwrite.io/support', - download_url='https://github.com/appwrite/sdk-for-python/archive/24.2.0.tar.gz', + download_url='https://github.com/appwrite/sdk-for-python/archive/24.3.0.tar.gz', install_requires=[ 'requests', 'pydantic>=2,<3', diff --git a/test/services/test_account.py b/test/services/test_account.py index ee88a7d..45da64b 100644 --- a/test/services/test_account.py +++ b/test/services/test_account.py @@ -733,6 +733,121 @@ def test_update_name(self, m): ) self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() + def test_list_passkeys(self, m): + data = { + "total": 5.0, + "passkeys": [], + } + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.list_passkeys() + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_create_passkey(self, m): + data = { + "$id": "bb8ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "passkeyId": "5e5ea5c16897e", + "expire": "2020-10-15T06:38:00.000+00:00", + "publicKey": {}, + } + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.create_passkey() + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_get_passkey(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "name": "My laptop", + "accessedAt": "2020-10-15T06:38:00.000+00:00", + } + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.get_passkey( + '', + ) + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_update_passkey(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "name": "My laptop", + "accessedAt": "2020-10-15T06:38:00.000+00:00", + } + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.update_passkey( + '', + '', + ) + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_delete_passkey(self, m): + data = '' + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.delete_passkey( + '', + ) + self.assertEqual(response, data) + + @requests_mock.Mocker() + def test_update_passkey_verification(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "name": "My laptop", + "accessedAt": "2020-10-15T06:38:00.000+00:00", + } + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.update_passkey_verification( + '', + {}, + ) + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_update_password(self, m): data = { @@ -1342,6 +1457,48 @@ def test_create_o_auth2_token(self, m): ) self.assertEqual(response, data) + @requests_mock.Mocker() + def test_create_passkey_token(self, m): + data = { + "$id": "bb8ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "passkeyId": "5e5ea5c16897e", + "expire": "2020-10-15T06:38:00.000+00:00", + "publicKey": {}, + } + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.create_passkey_token() + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_update_passkey_token(self, m): + data = { + "$id": "bb8ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "userId": "5e5ea5c168bb8", + "secret": "", + "expire": "2020-10-15T06:38:00.000+00:00", + "phrase": "Golden Fox", + } + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.update_passkey_token( + '', + {}, + ) + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_create_phone_token(self, m): data = { diff --git a/test/services/test_project.py b/test/services/test_project.py index 09549bf..f2a1100 100644 --- a/test/services/test_project.py +++ b/test/services/test_project.py @@ -1845,6 +1845,49 @@ def test_update_mfa_factors_policy(self, m): response = self.project.update_mfa_factors_policy() self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() + def test_update_passkey_policy(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "name": "New Project", + "teamId": "1592981250", + "region": "fra", + "devKeys": [], + "smtpEnabled": True, + "smtpSenderName": "John Appwrite", + "smtpSenderEmail": "john@appwrite.io", + "smtpReplyToName": "Support Team", + "smtpReplyToEmail": "support@appwrite.io", + "smtpHost": "mail.appwrite.io", + "smtpPort": 25.0, + "smtpUsername": "emailuser", + "smtpPassword": "smtp-password", + "smtpSecure": "tls", + "pingCount": 1.0, + "pingedAt": "2020-10-15T06:38:00.000+00:00", + "labels": [], + "status": "active", + "onboarding": {}, + "authMethods": [], + "services": [], + "protocols": [], + "blocks": [], + "consoleAccessedAt": "2020-10-15T06:38:00.000+00:00", + } + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.project.update_passkey_policy( + '', + ) + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_update_password_dictionary_policy(self, m): data = { diff --git a/test/services/test_users.py b/test/services/test_users.py index 2228892..d89f4a6 100644 --- a/test/services/test_users.py +++ b/test/services/test_users.py @@ -835,6 +835,85 @@ def test_update_name(self, m): ) self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() + def test_list_passkeys(self, m): + data = { + "total": 5.0, + "passkeys": [], + } + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.users.list_passkeys( + '', + ) + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_get_passkey(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "name": "My laptop", + "accessedAt": "2020-10-15T06:38:00.000+00:00", + } + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.users.get_passkey( + '', + '', + ) + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_update_passkey(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "name": "My laptop", + "accessedAt": "2020-10-15T06:38:00.000+00:00", + } + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.users.update_passkey( + '', + '', + '', + ) + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_delete_passkey(self, m): + data = '' + headers = {'Content-Type': 'application/json'} + m.request( + requests_mock.ANY, + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.users.delete_passkey( + '', + '', + ) + self.assertEqual(response, data) + @requests_mock.Mocker() def test_update_password(self, m): data = {