diff --git a/CHANGELOG.md b/CHANGELOG.md index 64f780c6..b00d409e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change Log +## 27.2.0 + +* Updated: `project.list_environments`, `project.create_environment`, `project.get_environment`, `project.update_environment` return `Environment` models instead of `Project` +* Added: `account.create_device`, `account.get_device`, `account.delete_device` and `account.update_device_session`, and the `Device` model +* Added: `project.update_account_devices_policy`, `ProjectPolicyId.ACCOUNT_DEVICES` and the `PolicyAccountDevices` model +* Added: `project.update_o_auth2_vipps`, `project.update_o_auth2_vipps_sandbox` and the `OAuth2Vipps` model +* Added: `OAuthProvider.VIPPS` and `OAuthProvider.VIPPSSANDBOX` +* Added: `proxy.update_function_rule`, `proxy.update_redirect_rule` and `proxy.update_site_rule` +* Added: optional `schedule` on `waf` rule create and update methods and on `WafRule*` models +* Added: `project.get_prefs` and `project.update_prefs` for project preferences +* Added: `Client.set_environment` to act in a project environment +* Added: `AppwriteMigrationResource.PROJECT_PREFERENCES` to migrate project preferences +* Added: `monitors.read`, `monitors.write`, `alerts.read`, `alerts.write`, `reviews.read` and `reviews.write` to `ProjectKeyScopes` +* Added: `Organization.budgetnotify`, `budgetcritical` and `educationexpiresat` +* Added: `Project.environmentid`, `Session.username` and `ActivityEvent.reason` +* Added: `ProxyRule.expireat` and `certificatestatus` +* Updated: requests send response format `2.7.1`; README targets Appwrite 2.7.x + ## 27.1.0 * Added: `migrations` service to run Appwrite, Firebase, NHost, Supabase, CSV and JSON migrations, plus retries and listing diff --git a/README.md b/README.md index 91ab58a5..ab249496 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Appwrite Python SDK ![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-2.6.2-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-2.7.1-blue.svg?style=flat-square) [![X Account](https://img.shields.io/badge/follow-%40appwrite-000000?logo=x&style=flat-square)](https://x.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) -**This SDK targets Appwrite server version 2.6.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-python/releases) when APIs differ. +**This SDK targets Appwrite server version 2.7.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-python/releases) when APIs differ. Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Python SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) diff --git a/appwrite/client.py b/appwrite/client.py index e187ea9b..cbb26b92 100644 --- a/appwrite/client.py +++ b/appwrite/client.py @@ -20,12 +20,12 @@ def __init__(self): self._endpoint = 'https://cloud.appwrite.io/v1' self._global_headers = { 'content-type': '', - 'user-agent': f'AppwritePythonSDK/27.1.0 ({platform.uname().system}; {platform.uname().version}; {platform.uname().machine})', + 'user-agent': f'AppwritePythonSDK/27.2.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': '27.1.0', - 'X-Appwrite-Response-Format': '2.6.2', + 'x-sdk-version': '27.2.0', + 'X-Appwrite-Response-Format': '2.7.1', } self._config = {} @@ -89,6 +89,13 @@ def set_locale(self, value): self._config['locale'] = value return self + def set_environment(self, value): + """The ID of the project environment to act in, alongside the project ID. Used with Appwrite Cloud projects that have environments.""" + + self._global_headers['x-appwrite-environment'] = value + self._config['environment'] = value + return self + def set_session(self, value): """The user session to authenticate with""" diff --git a/appwrite/enums/appwrite_migration_resource.py b/appwrite/enums/appwrite_migration_resource.py index c9cecc84..0b89e7d2 100644 --- a/appwrite/enums/appwrite_migration_resource.py +++ b/appwrite/enums/appwrite_migration_resource.py @@ -38,6 +38,7 @@ class AppwriteMigrationResource(Enum): PROJECT_VARIABLE = "project-variable" PROJECT_PROTOCOLS = "project-protocols" PROJECT_LABELS = "project-labels" + PROJECT_PREFERENCES = "project-preferences" PROJECT_SERVICES = "project-services" PROJECT_EMAIL_TEMPLATE = "project-email-template" PROJECT_ENVIRONMENT = "project-environment" diff --git a/appwrite/enums/o_auth_provider.py b/appwrite/enums/o_auth_provider.py index 87c6ed02..8c3f4b58 100644 --- a/appwrite/enums/o_auth_provider.py +++ b/appwrite/enums/o_auth_provider.py @@ -44,6 +44,8 @@ class OAuthProvider(Enum): TRADESHIFT = "tradeshift" TRADESHIFTBOX = "tradeshiftBox" TWITCH = "twitch" + VIPPS = "vipps" + VIPPSSANDBOX = "vippsSandbox" WEBFLOW = "webflow" WORDPRESS = "wordpress" X = "x" diff --git a/appwrite/enums/project_key_scopes.py b/appwrite/enums/project_key_scopes.py index 9168740c..fd444a37 100644 --- a/appwrite/enums/project_key_scopes.py +++ b/appwrite/enums/project_key_scopes.py @@ -90,6 +90,10 @@ class ProjectKeyScopes(Enum): RULES_WRITE = "rules.write" WEBHOOKS_READ = "webhooks.read" WEBHOOKS_WRITE = "webhooks.write" + MONITORS_READ = "monitors.read" + MONITORS_WRITE = "monitors.write" + ALERTS_READ = "alerts.read" + ALERTS_WRITE = "alerts.write" LOCALE_READ = "locale.read" AVATARS_READ = "avatars.read" AVATARS_WRITE = "avatars.write" @@ -105,6 +109,8 @@ class ProjectKeyScopes(Enum): INSIGHTS_WRITE = "insights.write" REPORTS_READ = "reports.read" REPORTS_WRITE = "reports.write" + REVIEWS_READ = "reviews.read" + REVIEWS_WRITE = "reviews.write" PRESENCES_READ = "presences.read" PRESENCES_WRITE = "presences.write" DEDICATEDDATABASES_EXECUTE = "dedicatedDatabases.execute" diff --git a/appwrite/enums/project_o_auth_provider_id.py b/appwrite/enums/project_o_auth_provider_id.py index 7757a41a..638a0abb 100644 --- a/appwrite/enums/project_o_auth_provider_id.py +++ b/appwrite/enums/project_o_auth_provider_id.py @@ -44,6 +44,8 @@ class ProjectOAuthProviderId(Enum): TRADESHIFT = "tradeshift" TRADESHIFTBOX = "tradeshiftBox" TWITCH = "twitch" + VIPPS = "vipps" + VIPPSSANDBOX = "vippsSandbox" WEBFLOW = "webflow" WORDPRESS = "wordpress" X = "x" diff --git a/appwrite/enums/project_policy_id.py b/appwrite/enums/project_policy_id.py index 5451fac3..ee27189c 100644 --- a/appwrite/enums/project_policy_id.py +++ b/appwrite/enums/project_policy_id.py @@ -21,3 +21,4 @@ class ProjectPolicyId(Enum): DENY_CORPORATE_EMAIL = "deny-corporate-email" DENY_REALTIME_GUESTS = "deny-realtime-guests" PASSKEY = "passkey" + ACCOUNT_DEVICES = "account-devices" diff --git a/appwrite/models/__init__.py b/appwrite/models/__init__.py index 1599e33e..54085966 100644 --- a/appwrite/models/__init__.py +++ b/appwrite/models/__init__.py @@ -108,6 +108,7 @@ from .account import Account from .preferences import Preferences from .session import Session +from .device import Device from .identity import Identity from .token import Token from .jwt import Jwt @@ -175,6 +176,7 @@ from .o_auth2_oidc import OAuth2Oidc from .o_auth2_okta import OAuth2Okta from .o_auth2_kick import OAuth2Kick +from .o_auth2_vipps import OAuth2Vipps from .o_auth2_apple import OAuth2Apple from .o_auth2_microsoft import OAuth2Microsoft from .o_auth2_resend import OAuth2Resend @@ -200,6 +202,7 @@ from .policy_deny_free_email import PolicyDenyFreeEmail from .policy_deny_corporate_email import PolicyDenyCorporateEmail from .policy_deny_realtime_guests import PolicyDenyRealtimeGuests +from .policy_account_devices import PolicyAccountDevices from .platform_web import PlatformWeb from .platform_apple import PlatformApple from .platform_android import PlatformAndroid @@ -273,6 +276,8 @@ from .dedicated_database_pooler import DedicatedDatabasePooler from .postgres_extension import PostgresExtension from .program import Program +from .environment import Environment +from .environment_list import EnvironmentList from .backup_restoration import BackupRestoration from .dedicated_database_restoration_list import DedicatedDatabaseRestorationList from .dedicated_database_specification import DedicatedDatabaseSpecification @@ -435,6 +440,7 @@ 'Account', 'Preferences', 'Session', + 'Device', 'Identity', 'Token', 'Jwt', @@ -502,6 +508,7 @@ 'OAuth2Oidc', 'OAuth2Okta', 'OAuth2Kick', + 'OAuth2Vipps', 'OAuth2Apple', 'OAuth2Microsoft', 'OAuth2Resend', @@ -527,6 +534,7 @@ 'PolicyDenyFreeEmail', 'PolicyDenyCorporateEmail', 'PolicyDenyRealtimeGuests', + 'PolicyAccountDevices', 'PlatformWeb', 'PlatformApple', 'PlatformAndroid', @@ -600,6 +608,8 @@ 'DedicatedDatabasePooler', 'PostgresExtension', 'Program', + 'Environment', + 'EnvironmentList', 'BackupRestoration', 'DedicatedDatabaseRestorationList', 'DedicatedDatabaseSpecification', diff --git a/appwrite/models/activity_event.py b/appwrite/models/activity_event.py index 69f3b03e..1c55d3c7 100644 --- a/appwrite/models/activity_event.py +++ b/appwrite/models/activity_event.py @@ -68,6 +68,8 @@ class ActivityEvent(AppwriteModel): Name of the SDK that triggered the event. sdkversion : str Version of the SDK that triggered the event. + reason : str + Why the actor made the request, as sent in the `X-Appwrite-Reason` header. Empty when no reason was given. """ id: str = Field(..., alias='$id') @@ -99,6 +101,7 @@ class ActivityEvent(AppwriteModel): hostname: str = Field(..., alias='hostname') sdk: str = Field(..., alias='sdk') sdkversion: str = Field(..., alias='sdkVersion') + reason: str = Field(..., alias='reason') @model_validator(mode='before') @classmethod @@ -165,4 +168,6 @@ def _apply_defaults(cls, data: Any) -> Any: data['sdk'] = '' if 'sdkVersion' not in data and 'sdkversion' not in data: data['sdkVersion'] = '' + if 'reason' not in data: + data['reason'] = '' return data diff --git a/appwrite/models/device.py b/appwrite/models/device.py new file mode 100644 index 00000000..cf47471c --- /dev/null +++ b/appwrite/models/device.py @@ -0,0 +1,59 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr, model_validator + +from .base_model import AppwriteModel +from .session import Session + + +class Device(AppwriteModel): + """ + Device + + Attributes + ---------- + id : str + Device ID. + createdat : str + Device creation date in ISO 8601 format. + updatedat : str + Device update date in ISO 8601 format. + expire : str + Device expiration date in ISO 8601 format. + activesessionid : str + ID of the session that is currently active on the device. Empty when no session is active, for example right after the active session was deleted. + sessions : List[Session] + List of sessions the device holds. Switch between them to change the active account. + secret : str + Secret used to authenticate the device. Carried by the device cookie; only included in the response if the request was made with an API key. + """ + + id: str = Field(..., alias='$id') + createdat: str = Field(..., alias='$createdAt') + updatedat: str = Field(..., alias='$updatedAt') + expire: str = Field(..., alias='expire') + activesessionid: str = Field(..., alias='activeSessionId') + sessions: List[Session] = Field(..., alias='sessions') + secret: str = Field(..., alias='secret') + + @model_validator(mode='before') + @classmethod + def _apply_defaults(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + + data = dict(data) + if '$id' not in data and 'id' not in data: + data['$id'] = '' + if '$createdAt' not in data and 'createdat' not in data: + data['$createdAt'] = '' + if '$updatedAt' not in data and 'updatedat' not in data: + data['$updatedAt'] = '' + if 'expire' not in data: + data['expire'] = '' + if 'activeSessionId' not in data and 'activesessionid' not in data: + data['activeSessionId'] = '' + if 'sessions' not in data: + data['sessions'] = [] + if 'secret' not in data: + data['secret'] = '' + return data diff --git a/appwrite/models/environment.py b/appwrite/models/environment.py new file mode 100644 index 00000000..c562ce80 --- /dev/null +++ b/appwrite/models/environment.py @@ -0,0 +1,48 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr, model_validator + +from .base_model import AppwriteModel + + +class Environment(AppwriteModel): + """ + Environment + + Attributes + ---------- + id : str + Environment ID, as passed in the X-Appwrite-Environment header. + createdat : str + Environment creation date in ISO 8601 format. + updatedat : str + Environment update date in ISO 8601 format. + projectid : str + ID of the project the environment belongs to, as passed in the X-Appwrite-Project header. + name : str + Environment name. + """ + + id: str = Field(..., alias='$id') + createdat: str = Field(..., alias='$createdAt') + updatedat: str = Field(..., alias='$updatedAt') + projectid: str = Field(..., alias='projectId') + name: str = Field(..., alias='name') + + @model_validator(mode='before') + @classmethod + def _apply_defaults(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + + data = dict(data) + if '$id' not in data and 'id' not in data: + data['$id'] = '' + if '$createdAt' not in data and 'createdat' not in data: + data['$createdAt'] = '' + if '$updatedAt' not in data and 'updatedat' not in data: + data['$updatedAt'] = '' + if 'projectId' not in data and 'projectid' not in data: + data['projectId'] = '' + if 'name' not in data: + data['name'] = '' + return data diff --git a/appwrite/models/environment_list.py b/appwrite/models/environment_list.py new file mode 100644 index 00000000..8b32d2ee --- /dev/null +++ b/appwrite/models/environment_list.py @@ -0,0 +1,34 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr, model_validator + +from .base_model import AppwriteModel +from .environment import Environment + + +class EnvironmentList(AppwriteModel): + """ + Environments list + + Attributes + ---------- + total : float + Total number of environments that matched your query. + environments : List[Environment] + List of environments. + """ + + total: float = Field(..., alias='total') + environments: List[Environment] = Field(..., alias='environments') + + @model_validator(mode='before') + @classmethod + def _apply_defaults(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + + data = dict(data) + if 'total' not in data: + data['total'] = 0 + if 'environments' not in data: + data['environments'] = [] + return data diff --git a/appwrite/models/o_auth2_provider_list.py b/appwrite/models/o_auth2_provider_list.py index 6d2fa3ec..5d3cbf20 100644 --- a/appwrite/models/o_auth2_provider_list.py +++ b/appwrite/models/o_auth2_provider_list.py @@ -49,6 +49,7 @@ from .o_auth2_cloudflare import OAuth2Cloudflare from .o_auth2_tik_tok import OAuth2TikTok from .o_auth2_kakao import OAuth2Kakao +from .o_auth2_vipps import OAuth2Vipps class OAuth2ProviderList(AppwriteModel): @@ -59,7 +60,7 @@ class OAuth2ProviderList(AppwriteModel): ---------- total : float Total number of OAuth2 providers in the given project. - providers : List[Union[OAuth2Github, OAuth2Discord, OAuth2Figma, OAuth2Dropbox, OAuth2Dailymotion, OAuth2Bitbucket, OAuth2Bitly, OAuth2Box, OAuth2Autodesk, OAuth2Google, OAuth2Zoom, OAuth2Zoho, OAuth2Yandex, OAuth2X, OAuth2WordPress, OAuth2Webflow, OAuth2Twitch, OAuth2Stripe, OAuth2Spotify, OAuth2Slack, OAuth2Podio, OAuth2Notion, OAuth2Salesforce, OAuth2Yahoo, OAuth2Linkedin, OAuth2Disqus, OAuth2Amazon, OAuth2Etsy, OAuth2Facebook, OAuth2Tradeshift, OAuth2Paypal, OAuth2Gitlab, OAuth2Appwrite, OAuth2Authentik, OAuth2Auth0, OAuth2FusionAuth, OAuth2Keycloak, OAuth2Oidc, OAuth2Apple, OAuth2Okta, OAuth2Kick, OAuth2Microsoft, OAuth2HuggingFace, OAuth2Resend, OAuth2Cloudflare, OAuth2TikTok, OAuth2Kakao]] + providers : List[Union[OAuth2Github, OAuth2Discord, OAuth2Figma, OAuth2Dropbox, OAuth2Dailymotion, OAuth2Bitbucket, OAuth2Bitly, OAuth2Box, OAuth2Autodesk, OAuth2Google, OAuth2Zoom, OAuth2Zoho, OAuth2Yandex, OAuth2X, OAuth2WordPress, OAuth2Webflow, OAuth2Twitch, OAuth2Stripe, OAuth2Spotify, OAuth2Slack, OAuth2Podio, OAuth2Notion, OAuth2Salesforce, OAuth2Yahoo, OAuth2Linkedin, OAuth2Disqus, OAuth2Amazon, OAuth2Etsy, OAuth2Facebook, OAuth2Tradeshift, OAuth2Paypal, OAuth2Gitlab, OAuth2Appwrite, OAuth2Authentik, OAuth2Auth0, OAuth2FusionAuth, OAuth2Keycloak, OAuth2Oidc, OAuth2Apple, OAuth2Okta, OAuth2Kick, OAuth2Microsoft, OAuth2HuggingFace, OAuth2Resend, OAuth2Cloudflare, OAuth2TikTok, OAuth2Kakao, OAuth2Vipps]] List of OAuth2 providers. """ @@ -113,6 +114,7 @@ class OAuth2ProviderList(AppwriteModel): OAuth2Cloudflare, OAuth2TikTok, OAuth2Kakao, + OAuth2Vipps, ] ] = Field(..., alias='providers') diff --git a/appwrite/models/o_auth2_vipps.py b/appwrite/models/o_auth2_vipps.py new file mode 100644 index 00000000..5ee21f87 --- /dev/null +++ b/appwrite/models/o_auth2_vipps.py @@ -0,0 +1,43 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr, model_validator + +from .base_model import AppwriteModel + + +class OAuth2Vipps(AppwriteModel): + """ + OAuth2Vipps + + Attributes + ---------- + id : str + OAuth2 provider ID. + enabled : bool + OAuth2 provider is active and can be used to create sessions. + clientid : str + Vipps MobilePay OAuth2 client ID. + clientsecret : str + Vipps MobilePay OAuth2 client secret. + """ + + id: str = Field(..., alias='$id') + enabled: bool = Field(..., alias='enabled') + clientid: str = Field(..., alias='clientId') + clientsecret: str = Field(..., alias='clientSecret') + + @model_validator(mode='before') + @classmethod + def _apply_defaults(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + + data = dict(data) + if '$id' not in data and 'id' not in data: + data['$id'] = '' + if 'enabled' not in data: + data['enabled'] = False + if 'clientId' not in data and 'clientid' not in data: + data['clientId'] = '' + if 'clientSecret' not in data and 'clientsecret' not in data: + data['clientSecret'] = '' + return data diff --git a/appwrite/models/organization.py b/appwrite/models/organization.py index b70f83f2..cf1d4ab5 100644 --- a/appwrite/models/organization.py +++ b/appwrite/models/organization.py @@ -31,6 +31,10 @@ class Organization(AppwriteModel, Generic[T]): Project budget limit. Null when no budget is set. budgetalerts : List[Any] Project budget limit + budgetnotify : List[Any] + Organization roles whose confirmed members are emailed when a budget alert fires. + budgetcritical : List[Any] + Budget alert percentages whose push notifications are critical. billingplan : str Organization's billing plan ID. billingplanid : str @@ -73,6 +77,8 @@ class Organization(AppwriteModel, Generic[T]): Program's discord channel name. programdiscordchannelurl : Optional[str] Program's discord channel URL. + educationexpiresat : Optional[str] + When the education plan ends, for an organization on it. billinglimits : Optional[BillingLimits] Billing limits reached billingplandowngrade : Optional[str] @@ -95,6 +101,8 @@ class Organization(AppwriteModel, Generic[T]): prefs: Preferences[T] = Field(..., alias='prefs') billingbudget: Optional[float] = Field(default=None, alias='billingBudget') budgetalerts: List[Any] = Field(..., alias='budgetAlerts') + budgetnotify: List[Any] = Field(..., alias='budgetNotify') + budgetcritical: List[Any] = Field(..., alias='budgetCritical') billingplan: str = Field(..., alias='billingPlan') billingplanid: str = Field(..., alias='billingPlanId') billingplandetails: BillingPlan = Field(..., alias='billingPlanDetails') @@ -116,6 +124,7 @@ class Organization(AppwriteModel, Generic[T]): programmanagercalendar: Optional[str] = Field(default=None, alias='programManagerCalendar') programdiscordchannelname: Optional[str] = Field(default=None, alias='programDiscordChannelName') programdiscordchannelurl: Optional[str] = Field(default=None, alias='programDiscordChannelUrl') + educationexpiresat: Optional[str] = Field(default=None, alias='educationExpiresAt') billinglimits: Optional[BillingLimits] = Field(default=None, alias='billingLimits') billingplandowngrade: Optional[str] = Field(default=None, alias='billingPlanDowngrade') billingtaxid: Optional[str] = Field(default=None, alias='billingTaxId') @@ -142,6 +151,10 @@ def _apply_defaults(cls, data: Any) -> Any: data['total'] = 0 if 'budgetAlerts' not in data and 'budgetalerts' not in data: data['budgetAlerts'] = [75] + if 'budgetNotify' not in data and 'budgetnotify' not in data: + data['budgetNotify'] = ['owner', 'billing'] + if 'budgetCritical' not in data and 'budgetcritical' not in data: + data['budgetCritical'] = [] if 'billingPlan' not in data and 'billingplan' not in data: data['billingPlan'] = 'tier-0' if 'billingPlanId' not in data and 'billingplanid' not in data: diff --git a/appwrite/models/policy_account_devices.py b/appwrite/models/policy_account_devices.py new file mode 100644 index 00000000..3ce0f354 --- /dev/null +++ b/appwrite/models/policy_account_devices.py @@ -0,0 +1,33 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr, model_validator + +from .base_model import AppwriteModel + + +class PolicyAccountDevices(AppwriteModel): + """ + Policy Account Devices + + Attributes + ---------- + id : str + Policy ID. + enabled : bool + Whether account devices are enabled. When disabled, new account devices can no longer be created. + """ + + id: str = Field(..., alias='$id') + enabled: bool = Field(..., alias='enabled') + + @model_validator(mode='before') + @classmethod + def _apply_defaults(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + + data = dict(data) + if '$id' not in data and 'id' not in data: + data['$id'] = '' + if 'enabled' not in data: + data['enabled'] = True + return data diff --git a/appwrite/models/policy_list.py b/appwrite/models/policy_list.py index 0109c6ff..982d26a0 100644 --- a/appwrite/models/policy_list.py +++ b/appwrite/models/policy_list.py @@ -21,6 +21,7 @@ from .policy_deny_corporate_email import PolicyDenyCorporateEmail from .policy_deny_realtime_guests import PolicyDenyRealtimeGuests from .policy_passkey import PolicyPasskey +from .policy_account_devices import PolicyAccountDevices class PolicyList(AppwriteModel): @@ -31,7 +32,7 @@ class PolicyList(AppwriteModel): ---------- total : float Total number of policies in the given project. - policies : List[Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicyPasswordPwned, PolicyPasswordRotation, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyMfaFactors, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail, PolicyDenyRealtimeGuests, PolicyPasskey]] + policies : List[Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicyPasswordPwned, PolicyPasswordRotation, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyMfaFactors, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail, PolicyDenyRealtimeGuests, PolicyPasskey, PolicyAccountDevices]] List of policies. """ @@ -57,6 +58,7 @@ class PolicyList(AppwriteModel): PolicyDenyCorporateEmail, PolicyDenyRealtimeGuests, PolicyPasskey, + PolicyAccountDevices, ] ] = Field(..., alias='policies') diff --git a/appwrite/models/project.py b/appwrite/models/project.py index f1420ab2..41ae1d6e 100644 --- a/appwrite/models/project.py +++ b/appwrite/models/project.py @@ -1,15 +1,18 @@ -from typing import Any, Dict, List, Optional, Union, cast +from typing import Any, Dict, List, Optional, Union, cast, Generic, TypeVar, Type from pydantic import Field, PrivateAttr, model_validator from .base_model import AppwriteModel +from .preferences import Preferences from .project_auth_method import ProjectAuthMethod from .project_service import ProjectService from .project_protocol import ProjectProtocol from .block import Block from .billing_limits import BillingLimits +T = TypeVar('T') -class Project(AppwriteModel): + +class Project(AppwriteModel, Generic[T]): """ Project @@ -53,6 +56,8 @@ class Project(AppwriteModel): Last ping datetime in ISO 8601 format. labels : List[Any] Labels for the project. + prefs : Optional[Preferences[T]] + Project preferences as a key-value object status : str Project status onboarding : Dict[str, Any] @@ -71,6 +76,8 @@ class Project(AppwriteModel): Whether WAF enforcement is enabled for the project. parentid : Optional[str] ID of the project this environment belongs to. Null when the project is not an environment. + environmentid : Optional[str] + ID of the environment, as passed in the X-Appwrite-Environment header, when the project is accessed as one. Null when the project is not an environment. billinglimits : Optional[BillingLimits] Billing limits reached firstaccessedat : Optional[str] @@ -132,6 +139,7 @@ class Project(AppwriteModel): pingcount: float = Field(..., alias='pingCount') pingedat: str = Field(..., alias='pingedAt') labels: List[Any] = Field(..., alias='labels') + prefs: Optional[Preferences[T]] = Field(default=None, alias='prefs') status: str = Field(..., alias='status') onboarding: Dict[str, Any] = Field(..., alias='onboarding') authmethods: List[ProjectAuthMethod] = Field(..., alias='authMethods') @@ -141,6 +149,7 @@ class Project(AppwriteModel): consoleaccessedat: str = Field(..., alias='consoleAccessedAt') wafenabled: Optional[bool] = Field(default=None, alias='wafEnabled') parentid: Optional[str] = Field(default=None, alias='parentId') + environmentid: Optional[str] = Field(default=None, alias='environmentId') billinglimits: Optional[BillingLimits] = Field(default=None, alias='billingLimits') firstaccessedat: Optional[str] = Field(default=None, alias='firstAccessedAt') mcpaccessedat: Optional[str] = Field(default=None, alias='mcpAccessedAt') @@ -234,3 +243,11 @@ def _apply_defaults(cls, data: Any) -> Any: if 'consoleAccessedAt' not in data and 'consoleaccessedat' not in data: data['consoleAccessedAt'] = '' return data + + @classmethod + def with_data(cls, data: Dict[str, Any], model_type: Type[T] = dict) -> 'Project[T]': + """Create Project instance with typed data.""" + instance = cls.model_validate(data) + if 'prefs' in data and data['prefs'] is not None: + instance.prefs = Preferences.with_data(data['prefs'], model_type) + return instance diff --git a/appwrite/models/project_list.py b/appwrite/models/project_list.py index 8e305afc..60f9246d 100644 --- a/appwrite/models/project_list.py +++ b/appwrite/models/project_list.py @@ -1,11 +1,13 @@ -from typing import Any, Dict, List, Optional, Union, cast +from typing import Any, Dict, List, Optional, Union, cast, Generic, TypeVar, Type from pydantic import Field, PrivateAttr, model_validator from .base_model import AppwriteModel from .project import Project +T = TypeVar('T') -class ProjectList(AppwriteModel): + +class ProjectList(AppwriteModel, Generic[T]): """ Projects List @@ -13,12 +15,12 @@ class ProjectList(AppwriteModel): ---------- total : float Total number of projects that matched your query. - projects : List[Project] + projects : List[Project[T]] List of projects. """ total: float = Field(..., alias='total') - projects: List[Project] = Field(..., alias='projects') + projects: List[Project[T]] = Field(..., alias='projects') @model_validator(mode='before') @classmethod @@ -32,3 +34,11 @@ def _apply_defaults(cls, data: Any) -> Any: if 'projects' not in data: data['projects'] = [] return data + + @classmethod + def with_data(cls, data: Dict[str, Any], model_type: Type[T] = dict) -> 'ProjectList[T]': + """Create ProjectList instance with typed data.""" + instance = cls.model_validate(data) + if 'projects' in data and data['projects'] is not None: + instance.projects = [Project.with_data(row, model_type) for row in data['projects']] + return instance diff --git a/appwrite/models/proxy_rule.py b/appwrite/models/proxy_rule.py index 0fdac26a..b225a502 100644 --- a/appwrite/models/proxy_rule.py +++ b/appwrite/models/proxy_rule.py @@ -43,6 +43,10 @@ class ProxyRule(AppwriteModel): Logs from rule verification or certificate generation. Certificate generation logs are prioritized if both are available. renewat : str Certificate auto-renewal date in ISO 8601 format. + expireat : str + Certificate expiry date in ISO 8601 format. Empty when unknown or not checked yet. + certificatestatus : str + Certificate status, as last reported by the certificate provider. Possible values are "pending", "processing", "issued", "renewing", "expiring", "blocked", "failed", or "unknown". "expiring" means the certificate expires soon, or has already expired, and the provider has not renewed it yet, "blocked" means the provider waits on a DNS record from the domain owner, and "failed" means the provider stopped trying or the last issuance or renewal attempt failed. Empty until the certificate is next checked. protocol : ProxyRuleProtocol Ingress protocol for the rule. """ @@ -65,6 +69,8 @@ class ProxyRule(AppwriteModel): status: ProxyRuleStatus = Field(..., alias='status') logs: str = Field(..., alias='logs') renewat: str = Field(..., alias='renewAt') + expireat: str = Field(..., alias='expireAt') + certificatestatus: str = Field(..., alias='certificateStatus') protocol: ProxyRuleProtocol = Field(..., alias='protocol') @model_validator(mode='before') @@ -100,6 +106,10 @@ def _apply_defaults(cls, data: Any) -> Any: data['status'] = 'unverified' if 'logs' not in data: data['logs'] = '' + if 'expireAt' not in data and 'expireat' not in data: + data['expireAt'] = '' + if 'certificateStatus' not in data and 'certificatestatus' not in data: + data['certificateStatus'] = '' if 'protocol' not in data: data['protocol'] = 'http' return data diff --git a/appwrite/models/session.py b/appwrite/models/session.py index ffcf6b5b..136a91c9 100644 --- a/appwrite/models/session.py +++ b/appwrite/models/session.py @@ -18,6 +18,8 @@ class Session(AppwriteModel): Session update date in ISO 8601 format. userid : str User ID. + username : str + Name of the user the session belongs to. Lets a client label the account, for example in an account switcher. expire : str Session expiration date in ISO 8601 format. provider : str @@ -74,6 +76,7 @@ class Session(AppwriteModel): createdat: str = Field(..., alias='$createdAt') updatedat: str = Field(..., alias='$updatedAt') userid: str = Field(..., alias='userId') + username: str = Field(..., alias='userName') expire: str = Field(..., alias='expire') provider: str = Field(..., alias='provider') provideruid: str = Field(..., alias='providerUid') @@ -115,6 +118,8 @@ def _apply_defaults(cls, data: Any) -> Any: data['$updatedAt'] = '' if 'userId' not in data and 'userid' not in data: data['userId'] = '' + if 'userName' not in data and 'username' not in data: + data['userName'] = '' if 'expire' not in data: data['expire'] = '' if 'provider' not in data: diff --git a/appwrite/models/waf_rule_bypass.py b/appwrite/models/waf_rule_bypass.py index 7134f681..31b8c710 100644 --- a/appwrite/models/waf_rule_bypass.py +++ b/appwrite/models/waf_rule_bypass.py @@ -39,6 +39,8 @@ class WafRuleBypass(AppwriteModel): List of conditions evaluated for this rule. config : Dict[str, Any] Action specific configuration. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime, endTime and timezone. Null when the rule is always active. """ id: str = Field(..., alias='$id') @@ -55,6 +57,7 @@ class WafRuleBypass(AppwriteModel): enabled: bool = Field(..., alias='enabled') conditions: List[Any] = Field(..., alias='conditions') config: Dict[str, Any] = Field(..., alias='config') + schedule: Optional[Dict[str, Any]] = Field(default=None, alias='schedule') @model_validator(mode='before') @classmethod diff --git a/appwrite/models/waf_rule_challenge.py b/appwrite/models/waf_rule_challenge.py index 0c7c7531..f75f6dba 100644 --- a/appwrite/models/waf_rule_challenge.py +++ b/appwrite/models/waf_rule_challenge.py @@ -39,6 +39,8 @@ class WafRuleChallenge(AppwriteModel): List of conditions evaluated for this rule. config : Dict[str, Any] Action specific configuration. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime, endTime and timezone. Null when the rule is always active. challengetype : str Challenge type enforced when the rule matches. difficulty : float @@ -61,6 +63,7 @@ class WafRuleChallenge(AppwriteModel): enabled: bool = Field(..., alias='enabled') conditions: List[Any] = Field(..., alias='conditions') config: Dict[str, Any] = Field(..., alias='config') + schedule: Optional[Dict[str, Any]] = Field(default=None, alias='schedule') challengetype: str = Field(..., alias='challengeType') difficulty: float = Field(..., alias='difficulty') ttl: float = Field(..., alias='ttl') diff --git a/appwrite/models/waf_rule_deny.py b/appwrite/models/waf_rule_deny.py index 8ce11701..e7c223bb 100644 --- a/appwrite/models/waf_rule_deny.py +++ b/appwrite/models/waf_rule_deny.py @@ -39,6 +39,8 @@ class WafRuleDeny(AppwriteModel): List of conditions evaluated for this rule. config : Dict[str, Any] Action specific configuration. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime, endTime and timezone. Null when the rule is always active. """ id: str = Field(..., alias='$id') @@ -55,6 +57,7 @@ class WafRuleDeny(AppwriteModel): enabled: bool = Field(..., alias='enabled') conditions: List[Any] = Field(..., alias='conditions') config: Dict[str, Any] = Field(..., alias='config') + schedule: Optional[Dict[str, Any]] = Field(default=None, alias='schedule') @model_validator(mode='before') @classmethod diff --git a/appwrite/models/waf_rule_rate_limit.py b/appwrite/models/waf_rule_rate_limit.py index 3e816549..38b287a3 100644 --- a/appwrite/models/waf_rule_rate_limit.py +++ b/appwrite/models/waf_rule_rate_limit.py @@ -39,6 +39,8 @@ class WafRuleRateLimit(AppwriteModel): List of conditions evaluated for this rule. config : Dict[str, Any] Action specific configuration. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime, endTime and timezone. Null when the rule is always active. limit : float Maximum number of matching requests allowed for the given interval. interval : float @@ -65,6 +67,7 @@ class WafRuleRateLimit(AppwriteModel): enabled: bool = Field(..., alias='enabled') conditions: List[Any] = Field(..., alias='conditions') config: Dict[str, Any] = Field(..., alias='config') + schedule: Optional[Dict[str, Any]] = Field(default=None, alias='schedule') limit: float = Field(..., alias='limit') interval: float = Field(..., alias='interval') key: str = Field(..., alias='key') diff --git a/appwrite/models/waf_rule_redirect.py b/appwrite/models/waf_rule_redirect.py index 434c49eb..da23d8c8 100644 --- a/appwrite/models/waf_rule_redirect.py +++ b/appwrite/models/waf_rule_redirect.py @@ -39,6 +39,8 @@ class WafRuleRedirect(AppwriteModel): List of conditions evaluated for this rule. config : Dict[str, Any] Action specific configuration. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime, endTime and timezone. Null when the rule is always active. location : str Target location for the redirect. statuscode : float @@ -59,6 +61,7 @@ class WafRuleRedirect(AppwriteModel): enabled: bool = Field(..., alias='enabled') conditions: List[Any] = Field(..., alias='conditions') config: Dict[str, Any] = Field(..., alias='config') + schedule: Optional[Dict[str, Any]] = Field(default=None, alias='schedule') location: str = Field(..., alias='location') statuscode: float = Field(..., alias='statusCode') diff --git a/appwrite/services/account.py b/appwrite/services/account.py index c71d2fe0..7be4175a 100644 --- a/appwrite/services/account.py +++ b/appwrite/services/account.py @@ -8,13 +8,14 @@ from ..models.oauth2_consent import Oauth2Consent from ..models.oauth2_consent_token_list import Oauth2ConsentTokenList from ..models.oauth2_consent_token import Oauth2ConsentToken +from ..models.device import Device +from ..models.session import Session from ..models.identity_list import IdentityList from ..models.membership_list import MembershipList from ..enums.authenticator_type import AuthenticatorType from ..models.mfa_type import MfaType from ..enums.authentication_factor import AuthenticationFactor from ..models.mfa_challenge import MfaChallenge -from ..models.session import Session from ..models.mfa_factors import MfaFactors from ..models.mfa_recovery_codes import MfaRecoveryCodes from ..models.passkey_list import PasskeyList @@ -403,6 +404,172 @@ def delete_consent_token( return response + def create_device( + self, + ) -> Device: + """ + Create an account switching device. A device is a container, scoped to one browser or app install, that holds multiple sessions and tracks which one is active. Once a device exists, session creation endpoints attach the new session to it automatically, so a single browser can stay signed in to several accounts and switch between them. A device holds at most one session per user: signing in again as a user it already holds replaces that user's session. The device lives as long as its newest session. + + The device secret is returned in a cookie. Run this on app start: if the browser already has a valid device, the existing one is returned with the same status code instead of an error, so it is always safe to call. + + Every session that joins the device rotates the device secret and re-issues the cookie, so an earlier copy of the secret stops working. A client that keeps the secret itself, rather than relying on the cookie, must replace it from each session response. + Returns + ------- + Device + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/devices' + 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=Device) + + def get_device( + self, + device_id: str, + ) -> Device: + """ + Get the account switching device for the current browser, including every session it holds, the name of the user each session belongs to, and the ID of the active session. Use the string 'current' to reference the current browser's device. + + Parameters + ---------- + device_id : str + Device ID. Use the string 'current' to reference the device of the current browser. + Returns + ------- + Device + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/devices/{deviceId}' + api_params = {} + if device_id is None or device_id == '': + raise AppwriteException('Missing required parameter: "device_id"') + api_path = api_path.replace('{deviceId}', self._encode_path(device_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=Device) + + def delete_device( + self, + device_id: str, + ) -> Dict[str, Any]: + """ + Delete the account switching device for the current browser. Every session the device holds is signed out, each firing its own session delete event, the device is removed, and both the device and session cookies are cleared. Use the string 'current' to reference the current browser's device. + + Parameters + ---------- + device_id : str + Device ID. Use the string 'current' to reference the device of the current browser. + Returns + ------- + Dict[str, Any] + API response as a dictionary + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/devices/{deviceId}' + api_params = {} + if device_id is None or device_id == '': + raise AppwriteException('Missing required parameter: "device_id"') + api_path = api_path.replace('{deviceId}', self._encode_path(device_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_device_session( + self, + device_id: str, + session_id: str, + ) -> Session: + """ + Switch the active account on an account switching device to one of the sessions it holds. The chosen session becomes active and its secret is written to the browser's session cookie, so subsequent requests authenticate as that account. Use the string 'current' to reference the current browser's device. + + Switching rotates the chosen session's secret. Any other holder of that session, such as a copy kept elsewhere, stops working, so a session should not be shared across devices. + + Parameters + ---------- + device_id : str + Device ID. Use the string 'current' to reference the device of the current browser. + session_id : str + ID of the session, held by the device, to make active. + Returns + ------- + Session + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/account/devices/{deviceId}/session' + api_params = {} + if device_id is None or device_id == '': + raise AppwriteException('Missing required parameter: "device_id"') + if session_id is None: + raise AppwriteException('Missing required parameter: "session_id"') + api_path = api_path.replace('{deviceId}', self._encode_path(device_id)) + api_params['sessionId'] = self._normalize_value(session_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=Session) + def update_email( self, email: str, @@ -2359,7 +2526,7 @@ def create_o_auth2_token( Parameters ---------- provider : OAuthProvider - OAuth2 Provider. Currently, supported providers are: amazon, apple, appwrite, auth0, authentik, autodesk, bitbucket, bitly, box, cloudflare, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, huggingface, kakao, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, resend, salesforce, slack, spotify, stripe, tiktok, tradeshift, tradeshiftBox, twitch, webflow, wordpress, x, yahoo, yammer, yandex, zoho, zoom. + OAuth2 Provider. Currently, supported providers are: amazon, apple, appwrite, auth0, authentik, autodesk, bitbucket, bitly, box, cloudflare, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, huggingface, kakao, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, resend, salesforce, slack, spotify, stripe, tiktok, tradeshift, tradeshiftBox, twitch, vipps, vippsSandbox, webflow, wordpress, x, yahoo, yammer, yandex, zoho, zoom. success : Optional[str] URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. failure : Optional[str] diff --git a/appwrite/services/domains.py b/appwrite/services/domains.py index 2d2d687b..b4946df4 100644 --- a/appwrite/services/domains.py +++ b/appwrite/services/domains.py @@ -1503,7 +1503,7 @@ def create_record_cname( domain_id : str Domain unique ID. name : str - Record name relative to the domain, such as `www`, or `@` for the domain itself. A full name ending in the domain is also accepted and stored relative to it. + Record name relative to the domain, such as `www`. A CNAME cannot be at the domain itself; use an ALIAS record there. A full name ending in the domain is also accepted and stored relative to it. value : str Canonical target for this CNAME record. ttl : float @@ -1570,7 +1570,7 @@ def update_record_cname( record_id : str DNS record unique ID. name : str - Record name relative to the domain, such as `www`, or `@` for the domain itself. A full name ending in the domain is also accepted and stored relative to it. + Record name relative to the domain, such as `www`. A CNAME cannot be at the domain itself; use an ALIAS record there. A full name ending in the domain is also accepted and stored relative to it. value : str Canonical target for this CNAME record. ttl : float diff --git a/appwrite/services/organization.py b/appwrite/services/organization.py index 7b2a9fcf..301589c7 100644 --- a/appwrite/services/organization.py +++ b/appwrite/services/organization.py @@ -604,7 +604,8 @@ def list_projects( queries: Optional[List[str]] = None, search: Optional[str] = None, total: Optional[bool] = None, - ) -> ProjectList: + model_type: Type[T] = dict, + ) -> ProjectList[T]: """ Get a list of all projects. You can use the query params to filter your results. @@ -616,9 +617,12 @@ def list_projects( Search term to filter your list results. Max length: 256 chars. total : Optional[bool] When set to false, the total count returned will be 0 and will not be calculated. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectList + ProjectList[T] API response as a typed Pydantic model Raises @@ -646,14 +650,15 @@ def list_projects( api_params, ) - return self._parse_response(response, model=ProjectList) + return ProjectList.with_data(response, model_type) def create_project( self, project_id: str, name: str, region: Optional[Region] = None, - ) -> Project: + model_type: Type[T] = dict, + ) -> Project[T]: """ Create a new project. @@ -665,9 +670,12 @@ def create_project( Project name. Max length: 128 chars. region : Optional[Region] Project Region. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - Project + Project[T] API response as a typed Pydantic model Raises @@ -698,12 +706,13 @@ def create_project( api_params, ) - return self._parse_response(response, model=Project) + return Project.with_data(response, model_type) def get_project( self, project_id: str, - ) -> Project: + model_type: Type[T] = dict, + ) -> Project[T]: """ Get a project. @@ -711,9 +720,12 @@ def get_project( ---------- project_id : str Project unique ID. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - Project + Project[T] API response as a typed Pydantic model Raises @@ -738,13 +750,14 @@ def get_project( api_params, ) - return self._parse_response(response, model=Project) + return Project.with_data(response, model_type) def update_project( self, project_id: str, name: str, - ) -> Project: + model_type: Type[T] = dict, + ) -> Project[T]: """ Update a project by its unique ID. @@ -754,9 +767,12 @@ def update_project( Project unique ID. name : str Project name. Max length: 128 chars. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - Project + Project[T] API response as a typed Pydantic model Raises @@ -785,7 +801,7 @@ def update_project( api_params, ) - return self._parse_response(response, model=Project) + return Project.with_data(response, model_type) def delete_project( self, diff --git a/appwrite/services/project.py b/appwrite/services/project.py index fb980b51..736944aa 100644 --- a/appwrite/services/project.py +++ b/appwrite/services/project.py @@ -1,12 +1,13 @@ from ..service import Service from urllib.parse import quote -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union, Type, TypeVar from ..exception import AppwriteException from appwrite.utils.deprecated import deprecated from ..models.project import Project as ProjectModel from ..enums.project_auth_method_id import ProjectAuthMethodId -from ..models.project_list import ProjectList +from ..models.environment_list import EnvironmentList from ..enums.environment_resource import EnvironmentResource +from ..models.environment import Environment from ..models.key_list import KeyList from ..enums.project_key_scopes import ProjectKeyScopes from ..models.ephemeral_key import EphemeralKey @@ -63,6 +64,7 @@ from ..models.o_auth2_tik_tok import OAuth2TikTok from ..models.o_auth2_tradeshift import OAuth2Tradeshift from ..models.o_auth2_twitch import OAuth2Twitch +from ..models.o_auth2_vipps import OAuth2Vipps from ..models.o_auth2_webflow import OAuth2Webflow from ..models.o_auth2_word_press import OAuth2WordPress from ..models.o_auth2_x import OAuth2X @@ -99,6 +101,8 @@ from ..models.policy_deny_corporate_email import PolicyDenyCorporateEmail from ..models.policy_deny_realtime_guests import PolicyDenyRealtimeGuests from ..models.policy_passkey import PolicyPasskey +from ..models.policy_account_devices import PolicyAccountDevices +from ..models.preferences import Preferences from ..enums.project_protocol_id import ProjectProtocolId from ..enums.project_service_id import ProjectServiceId from ..enums.project_smtp_secure import ProjectSMTPSecure @@ -109,6 +113,8 @@ from ..models.variable_list import VariableList from ..models.variable import Variable +T = TypeVar('T') + class Project(Service): @@ -117,12 +123,19 @@ def __init__(self, client) -> None: def get( self, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Get a project. + + Parameters + ---------- + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -144,7 +157,7 @@ def get( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def delete( self, @@ -182,7 +195,8 @@ def update_auth_method( self, method_id: ProjectAuthMethodId, enabled: bool, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Update properties of a specific auth method. Use this endpoint to enable or disable a method in your project. @@ -192,9 +206,12 @@ def update_auth_method( Auth Method ID. Possible values: email-password,magic-url,email-otp,anonymous,invites,jwt,phone,passkey enabled : bool Auth method status. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -223,25 +240,25 @@ def update_auth_method( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def list_environments( self, queries: Optional[List[str]] = None, total: Optional[bool] = None, - ) -> ProjectList: + ) -> EnvironmentList: """ - Get a list of all environments of the project. You can use the query params to filter your results. + Get a list of all environments of the project. You can use the query params to filter your results. An environment is listed by its ID, the one passed in the `X-Appwrite-Environment` header to act in it. 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 on the following attributes: name, teamId, labels, search, accessedAt, status, parentId + 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 on the following attributes: $id, name, search total : Optional[bool] When set to false, the total count returned will be 0 and will not be calculated. Returns ------- - ProjectList + EnvironmentList API response as a typed Pydantic model Raises @@ -267,30 +284,30 @@ def list_environments( api_params, ) - return self._parse_response(response, model=ProjectList) + return self._parse_response(response, model=EnvironmentList) def create_environment( self, environment_id: str, name: str, resources: Optional[List[EnvironmentResource]] = None, - ) -> ProjectModel: + ) -> Environment: """ - Create a new environment of the project. An environment is a separate project in the same organization and region, with its own resources and users. Its usage is billed under the project it belongs to. + Create a new environment of the project. An environment is a separate project in the same organization and region, with its own resources and users. Its usage is billed under the project it belongs to. Requests act in the environment when they carry the project's ID in the `X-Appwrite-Project` header and the environment's ID in the `X-Appwrite-Environment` header; the environment has no project ID of its own to pass. Pass `resources` to copy the project into the environment. The environment is returned at once and the copy runs as a migration of the environment, listed by its migrations routes and announced on its `migrations.*` realtime events; a resource whose ID can be chosen keeps it. Users are copied newest first and at most 90 of them, so the environment keeps room under its limit of 100 users for test accounts, and the memberships of users left behind are not copied. Parameters ---------- environment_id : str - Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can't start with a special char. Max length is 36 chars. + Environment ID, unique within the project and passed in the `X-Appwrite-Environment` header to act in the environment. Choose a custom ID such as `staging`, or generate a random ID with `ID.unique()`. Valid chars are a-z, 0-9, and hyphen. Can't start with a special char. Max length is 36 chars. name : str Environment name. Max length: 128 chars. resources : Optional[List[EnvironmentResource]] Resources to copy from the project into the environment, by type. A structural type such as `database`, `table`, `column`, `index`, `bucket`, `team`, `function` or `site` copies the structure and its settings; `row`, `file`, `user`, `membership`, `deployment` and `site-deployment` copy what it holds. A function or site is copied with its active deployment; add `deployment` or `site-deployment` to copy every deployment. Users are copied newest first and at most 90 of them, leaving room under the limit of 100 users of an environment for test accounts; the memberships of users left behind are not copied. Leave empty for an empty environment. Maximum of 100 types. Returns ------- - ProjectModel + Environment API response as a typed Pydantic model Raises @@ -321,22 +338,22 @@ def create_environment( api_params, ) - return self._parse_response(response, model=ProjectModel) + return self._parse_response(response, model=Environment) def get_environment( self, environment_id: str, - ) -> ProjectModel: + ) -> Environment: """ - Get an environment of the project by its unique ID. + Get an environment of the project by its ID, the one passed in the `X-Appwrite-Environment` header to act in it. Parameters ---------- environment_id : str - Environment unique ID. + Environment ID. Returns ------- - ProjectModel + Environment API response as a typed Pydantic model Raises @@ -361,25 +378,25 @@ def get_environment( api_params, ) - return self._parse_response(response, model=ProjectModel) + return self._parse_response(response, model=Environment) def update_environment( self, environment_id: str, name: str, - ) -> ProjectModel: + ) -> Environment: """ - Update the name of an environment of the project. + Update the name of an environment of the project. The environment ID cannot be changed. Parameters ---------- environment_id : str - Environment unique ID. + Environment ID. name : str Environment name. Max length: 128 chars. Returns ------- - ProjectModel + Environment API response as a typed Pydantic model Raises @@ -408,7 +425,7 @@ def update_environment( api_params, ) - return self._parse_response(response, model=ProjectModel) + return self._parse_response(response, model=Environment) def delete_environment( self, @@ -420,7 +437,7 @@ def delete_environment( Parameters ---------- environment_id : str - Environment unique ID. + Environment ID. Returns ------- Dict[str, Any] @@ -691,7 +708,8 @@ def delete_key( def update_labels( self, labels: List[str], - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Update the project labels. Labels can be used to easily filter projects in an organization. @@ -699,9 +717,12 @@ def update_labels( ---------- labels : List[str] Array of project labels. Replaces the previous labels. Maximum of 1000 labels are allowed, each up to 36 alphanumeric characters long. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -727,7 +748,7 @@ def update_labels( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def list_mock_phones( self, @@ -1010,7 +1031,8 @@ def update_o_auth2_server( device_code_duration: Optional[float] = None, default_scopes: Optional[List[str]] = None, installation_scopes: Optional[List[str]] = None, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Update the OAuth2 server (OIDC provider) configuration. @@ -1048,9 +1070,12 @@ def update_o_auth2_server( List of OAuth2 scopes used when an authorization request omits the scope parameter. Every default scope must also be allowed by the OAuth2 server. Maximum of 100 scopes are allowed, each up to 128 characters long. installation_scopes : Optional[List[str]] List of scopes an application may request when installed on a team. Omitting the parameter clears the list, so no installation scopes can be granted. Maximum of 100 scopes are allowed, each up to 128 characters long. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -1107,7 +1132,7 @@ def update_o_auth2_server( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_o_auth2_amazon( self, @@ -3354,6 +3379,106 @@ def update_o_auth2_twitch( return self._parse_response(response, model=OAuth2Twitch) + def update_o_auth2_vipps( + self, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + enabled: Optional[bool] = None, + ) -> OAuth2Vipps: + """ + Update the project OAuth2 Vipps MobilePay configuration. + + Parameters + ---------- + client_id : Optional[str] + 'Client ID' of Vipps MobilePay OAuth2 app. For example: 9a3a4b3e-0000-0000-0000-0f1a6d1c3b2e + client_secret : Optional[str] + 'Client Secret' of Vipps MobilePay OAuth2 app. For example: Ay1jb000000000000000000000000000000Vx3k + enabled : Optional[bool] + OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid. + Returns + ------- + OAuth2Vipps + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/project/oauth2/vipps' + api_params = {} + if client_id is not None: + api_params['clientId'] = self._normalize_value(client_id) + if client_secret is not None: + api_params['clientSecret'] = self._normalize_value(client_secret) + if enabled is not None: + api_params['enabled'] = self._normalize_value(enabled) + + 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=OAuth2Vipps) + + def update_o_auth2_vipps_sandbox( + self, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + enabled: Optional[bool] = None, + ) -> OAuth2Vipps: + """ + Update the project OAuth2 Vipps MobilePay Sandbox configuration. + + Parameters + ---------- + client_id : Optional[str] + 'Client ID' of Vipps MobilePay Sandbox OAuth2 app. For example: 9a3a4b3e-0000-0000-0000-0f1a6d1c3b2e + client_secret : Optional[str] + 'Client Secret' of Vipps MobilePay Sandbox OAuth2 app. For example: Ay1jb000000000000000000000000000000Vx3k + enabled : Optional[bool] + OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid. + Returns + ------- + OAuth2Vipps + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/project/oauth2/vippsSandbox' + api_params = {} + if client_id is not None: + api_params['clientId'] = self._normalize_value(client_id) + if client_secret is not None: + api_params['clientSecret'] = self._normalize_value(client_secret) + if enabled is not None: + api_params['enabled'] = self._normalize_value(enabled) + + 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=OAuth2Vipps) + def update_o_auth2_webflow( self, client_id: Optional[str] = None, @@ -3759,6 +3884,7 @@ def get_o_auth2_provider( OAuth2Microsoft, OAuth2TikTok, OAuth2Kakao, + OAuth2Vipps, ]: """ Get a single OAuth2 provider configuration. Credential fields (client secret, p8 file, key/team IDs) are write-only and always returned empty. @@ -3769,7 +3895,7 @@ def get_o_auth2_provider( OAuth2 provider key. For example: github, google, apple. Returns ------- - Union[OAuth2Github, OAuth2Discord, OAuth2Figma, OAuth2Dropbox, OAuth2Dailymotion, OAuth2Bitbucket, OAuth2Bitly, OAuth2Box, OAuth2Autodesk, OAuth2Google, OAuth2Zoom, OAuth2Zoho, OAuth2Yandex, OAuth2X, OAuth2WordPress, OAuth2Webflow, OAuth2Twitch, OAuth2Stripe, OAuth2Spotify, OAuth2Slack, OAuth2Podio, OAuth2Notion, OAuth2Salesforce, OAuth2Yahoo, OAuth2HuggingFace, OAuth2Resend, OAuth2Cloudflare, OAuth2Linkedin, OAuth2Disqus, OAuth2Amazon, OAuth2Etsy, OAuth2Facebook, OAuth2Tradeshift, OAuth2Paypal, OAuth2Gitlab, OAuth2Authentik, OAuth2Auth0, OAuth2FusionAuth, OAuth2Keycloak, OAuth2Oidc, OAuth2Apple, OAuth2Okta, OAuth2Kick, OAuth2Microsoft, OAuth2TikTok, OAuth2Kakao] + Union[OAuth2Github, OAuth2Discord, OAuth2Figma, OAuth2Dropbox, OAuth2Dailymotion, OAuth2Bitbucket, OAuth2Bitly, OAuth2Box, OAuth2Autodesk, OAuth2Google, OAuth2Zoom, OAuth2Zoho, OAuth2Yandex, OAuth2X, OAuth2WordPress, OAuth2Webflow, OAuth2Twitch, OAuth2Stripe, OAuth2Spotify, OAuth2Slack, OAuth2Podio, OAuth2Notion, OAuth2Salesforce, OAuth2Yahoo, OAuth2HuggingFace, OAuth2Resend, OAuth2Cloudflare, OAuth2Linkedin, OAuth2Disqus, OAuth2Amazon, OAuth2Etsy, OAuth2Facebook, OAuth2Tradeshift, OAuth2Paypal, OAuth2Gitlab, OAuth2Authentik, OAuth2Auth0, OAuth2FusionAuth, OAuth2Keycloak, OAuth2Oidc, OAuth2Apple, OAuth2Okta, OAuth2Kick, OAuth2Microsoft, OAuth2TikTok, OAuth2Kakao, OAuth2Vipps] API response as one of the typed response models Raises @@ -3892,9 +4018,15 @@ def get_o_auth2_provider( if response.get('$id') == 'facebook': return self._parse_response(response, model=OAuth2Facebook) + if response.get('$id') == 'tradeshift': + return self._parse_response(response, model=OAuth2Tradeshift) + if response.get('$id') == 'tradeshiftBox': return self._parse_response(response, model=OAuth2Tradeshift) + if response.get('$id') == 'paypal': + return self._parse_response(response, model=OAuth2Paypal) + if response.get('$id') == 'paypalSandbox': return self._parse_response(response, model=OAuth2Paypal) @@ -3934,6 +4066,12 @@ def get_o_auth2_provider( if response.get('$id') == 'kakao': return self._parse_response(response, model=OAuth2Kakao) + if response.get('$id') == 'vipps': + return self._parse_response(response, model=OAuth2Vipps) + + if response.get('$id') == 'vippsSandbox': + return self._parse_response(response, model=OAuth2Vipps) + raise AppwriteException('Unable to match response to any known model') def list_platforms( @@ -4668,10 +4806,56 @@ def list_policies( return self._parse_response(response, model=PolicyList) + def update_account_devices_policy( + self, + enabled: bool, + model_type: Type[T] = dict, + ) -> ProjectModel[T]: + """ + Update whether account devices are enabled for the project. When enabled, a browser can create an account device that holds multiple sessions and switch between them. When disabled, account devices can no longer be created, so each browser is limited to a single active session. + + Parameters + ---------- + enabled : bool + Set to true to allow account devices, or false to disable them for this project. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + + Returns + ------- + ProjectModel[T] + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/project/policies/account-devices' + api_params = {} + if enabled is None: + raise AppwriteException('Missing required parameter: "enabled"') + api_params['enabled'] = self._normalize_value(enabled) + + 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 ProjectModel.with_data(response, model_type) + def update_deny_aliased_email_policy( self, enabled: bool, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Configures if aliased emails such as subaddresses and emails with suffixes are denied during new users sign-ups and email updates. @@ -4679,9 +4863,12 @@ def update_deny_aliased_email_policy( ---------- enabled : bool Set whether or not to block aliased emails during signup and email updates. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -4707,12 +4894,13 @@ def update_deny_aliased_email_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_deny_corporate_email_policy( self, enabled: bool, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Configures if only corporate email addresses (non-free and non-disposable domains) are allowed during new user sign-ups and email updates. @@ -4720,9 +4908,12 @@ def update_deny_corporate_email_policy( ---------- enabled : bool Set whether or not to restrict sign-ups and email updates to corporate email addresses only. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -4748,12 +4939,13 @@ def update_deny_corporate_email_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_deny_disposable_email_policy( self, enabled: bool, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Configures if disposable emails from known temporary domains are denied during new users sign-ups and email updates. @@ -4761,9 +4953,12 @@ def update_deny_disposable_email_policy( ---------- enabled : bool Set whether or not to block disposable email addresses during signup and email updates. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -4789,12 +4984,13 @@ def update_deny_disposable_email_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_deny_free_email_policy( self, enabled: bool, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Configures if emails from free providers such as Gmail or Yahoo are denied during new users sign-ups and email updates. @@ -4802,9 +4998,12 @@ def update_deny_free_email_policy( ---------- enabled : bool Set whether or not to block free email addresses during signup and email updates. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -4830,12 +5029,13 @@ def update_deny_free_email_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_deny_realtime_guests_policy( self, enabled: bool, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Configures if guests, connections without a signed-in user, may subscribe to realtime channels. When enabled, a guest can still connect and sign in over the connection, but cannot subscribe until it does. @@ -4843,9 +5043,12 @@ def update_deny_realtime_guests_policy( ---------- enabled : bool Set whether or not to stop guests from subscribing to realtime channels. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -4871,7 +5074,7 @@ def update_deny_realtime_guests_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_membership_privacy_policy( self, @@ -4881,7 +5084,8 @@ def update_membership_privacy_policy( user_name: Optional[bool] = None, user_mfa: Optional[bool] = None, user_accessed_at: Optional[bool] = None, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Updating this policy allows you to control if team members can see other members information. When enabled, all team members can see ID, name, email, phone number, and MFA status of other members.. @@ -4899,9 +5103,12 @@ def update_membership_privacy_policy( Set to true if you want make user MFA status visible to all team members, or false to hide it. user_accessed_at : Optional[bool] Set to true if you want make user last access time visible to all team members, or false to hide it. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -4936,7 +5143,7 @@ def update_membership_privacy_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_mfa_factors_policy( self, @@ -4944,7 +5151,8 @@ def update_mfa_factors_policy( email: Optional[bool] = None, phone: Optional[bool] = None, custom: Optional[bool] = None, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Updating this policy allows you to control which factors users can use to complete an MFA challenge. Disabled factors cannot be used to create a challenge and are reported as unavailable when listing factors. The custom factor is disabled by default; enable it to deliver challenge codes through your own channel. Recovery codes always remain available as a fallback. @@ -4958,9 +5166,12 @@ def update_mfa_factors_policy( Set to true to allow phone (SMS) to complete an MFA challenge, or false to disable it. custom : Optional[bool] Set to true to allow the custom factor to complete an MFA challenge, or false to disable it. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -4991,12 +5202,13 @@ def update_mfa_factors_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_passkey_policy( self, rp_id: str, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ 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. @@ -5004,9 +5216,12 @@ def update_passkey_policy( ---------- 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. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5032,13 +5247,14 @@ def update_passkey_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_password_dictionary_policy( self, enabled: bool, deny_list: Optional[List[str]] = None, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Updating this policy allows you to control if new passwords are checked against most common passwords dictionary. When enabled, and user changes their password, password must not be contained in the dictionary. @@ -5050,9 +5266,12 @@ def update_password_dictionary_policy( Toggle password dictionary policy. Set to true if you want password change to block passwords in the dictionary, or false to allow them. When changing this policy, existing passwords remain valid. deny_list : Optional[List[str]] Custom blocked values using case-sensitive equality. Up to 100 entries of 1 to 256 characters. Omit or pass null to preserve, an array to replace, or [] to clear. Combined authentication settings must fit within 16,384 characters. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5080,12 +5299,13 @@ def update_password_dictionary_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_password_history_policy( self, total: Optional[float], - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Updates one of password strength policies. Based on total length configured, previous password hashes are stored, and users cannot choose a new password that is already stored in the passwird history list, when updating an user password, or setting new one through password recovery. @@ -5095,9 +5315,12 @@ def update_password_history_policy( ---------- total : Optional[float] Set the password history length per user. Value can be between 1 and 20, or null to disable the limit. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5121,13 +5344,14 @@ def update_password_history_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_password_personal_data_policy( self, enabled: bool, deny_list: Optional[List[str]] = None, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Updating this policy allows you to control if password strength is checked against personal data. When enabled, and user sets or changes their password, the password must not contain user ID, name, email or phone number. @@ -5140,9 +5364,12 @@ def update_password_personal_data_policy( Toggle password personal data policy. Set to true if you want to block passwords including user's personal data, or false to allow it. When changing this policy, existing passwords remain valid. deny_list : Optional[List[str]] Custom blocked values using case-insensitive substring matching. Up to 100 entries of 1 to 256 characters. Omit or pass null to preserve, an array to replace, or [] to clear. Combined authentication settings must fit within 16,384 characters. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5170,14 +5397,15 @@ def update_password_personal_data_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_password_pwned_policy( self, enabled: Optional[bool] = None, sessions: Optional[bool] = None, users: Optional[bool] = None, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Updating this policy allows you to control if passwords are checked against the Have I Been Pwned breach database. When enabled, every password a user signs up, signs in or resets with is checked and the result is recorded on the user as `passwordPwned`. On its own the policy only records. Enable `users` to reject a breached password when a user signs up or sets a new password, and `sessions` to refuse a sign-in with a breached password until it is reset. Only the first five characters of the password hash are ever shared with the service. Options left out keep their current value. @@ -5189,9 +5417,12 @@ def update_password_pwned_policy( Whether a sign-in with a breached password is refused until the password is reset. Default is false, which allows the sign-in and only records the result. users : Optional[bool] Whether a breached password is rejected when a user signs up or sets a new password. Default is false, which allows the password and only records the result. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5220,13 +5451,14 @@ def update_password_pwned_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_password_rotation_policy( self, enabled: Optional[bool] = None, max_age: Optional[float] = None, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Require a password reset when an email/password sign-in uses a password older than the configured maximum age. Options left out keep their current value. @@ -5236,9 +5468,12 @@ def update_password_rotation_policy( Enable password rotation. Disabled by default. max_age : Optional[float] Maximum password age in seconds. Minimum allowed value is 86400 seconds (1 day), and maximum is 10 years, which is 315360000 seconds. Defaults to 1 year, which is 31536000 seconds. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5265,7 +5500,7 @@ def update_password_rotation_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_password_strength_policy( self, @@ -5330,7 +5565,8 @@ def update_password_strength_policy( def update_session_alert_policy( self, enabled: bool, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Updating this policy allows you to control if email alert is sent upon session creation. When enabled, and user signs into their account, they will be sent an email notification. There is an exception, the first session after a new sign up does not trigger an alert, even if the policy is enabled. @@ -5338,9 +5574,12 @@ def update_session_alert_policy( ---------- enabled : bool Toggle session alert policy. Set to true if you want users to receive email notifications when a sessions are created for their users, or false to not send email alerts. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5366,12 +5605,13 @@ def update_session_alert_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_session_duration_policy( self, duration: float, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Update maximum duration how long sessions created within a project should stay active for. @@ -5379,9 +5619,12 @@ def update_session_duration_policy( ---------- duration : float Maximum session length in seconds. Minium allowed value is 60 seconds, and maximum is 1 year, which is 31536000 seconds. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5407,12 +5650,13 @@ def update_session_duration_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_session_invalidation_policy( self, enabled: bool, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Updating this policy allows you to control if existing sessions should be invalidated when a password of a user is changed. When enabled, and user changes their password, they will be logged out of all their devices. @@ -5420,9 +5664,12 @@ def update_session_invalidation_policy( ---------- enabled : bool Toggle session invalidation policy. Set to true if you want password change to invalidate all sessions of an user, or false to keep sessions active. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5448,12 +5695,13 @@ def update_session_invalidation_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_session_limit_policy( self, total: float, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Update the maximum number of sessions allowed per user. When the limit is hit, the oldest session will be deleted to make room for new one. @@ -5461,9 +5709,12 @@ def update_session_limit_policy( ---------- total : float Set the maximum number of sessions allowed per user. Value can be between 1 and 100. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5489,12 +5740,13 @@ def update_session_limit_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_user_limit_policy( self, total: Optional[float], - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Update the maximum number of users in the project. When the limit is hit or amount of existing users already exceeded the limit, all users remain active, but new user sign up will be prohibited. @@ -5502,9 +5754,12 @@ def update_user_limit_policy( ---------- total : Optional[float] Set the maximum number of users allowed in the project. Value can be between 0 and 10000. Use 0 or null to disable the limit. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5528,7 +5783,7 @@ def update_user_limit_policy( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def get_policy( self, @@ -5553,6 +5808,7 @@ def get_policy( PolicyDenyCorporateEmail, PolicyDenyRealtimeGuests, PolicyPasskey, + PolicyAccountDevices, ]: """ Get a policy by its unique ID. This endpoint returns the current configuration for the requested project policy. @@ -5560,10 +5816,10 @@ def get_policy( Parameters ---------- policy_id : ProjectPolicyId - Policy ID. Can be one of: password-dictionary, password-history, password-strength, password-personal-data, password-pwned, password-rotation, session-alert, session-duration, session-invalidation, session-limit, user-limit, membership-privacy, mfa-factors, deny-aliased-email, deny-disposable-email, deny-free-email, deny-corporate-email, deny-realtime-guests, passkey. + Policy ID. Can be one of: password-dictionary, password-history, password-strength, password-personal-data, password-pwned, password-rotation, session-alert, session-duration, session-invalidation, session-limit, user-limit, membership-privacy, mfa-factors, deny-aliased-email, deny-disposable-email, deny-free-email, deny-corporate-email, deny-realtime-guests, passkey, account-devices. Returns ------- - Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicyPasswordPwned, PolicyPasswordRotation, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyMfaFactors, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail, PolicyDenyRealtimeGuests, PolicyPasskey] + Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicyPasswordPwned, PolicyPasswordRotation, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyMfaFactors, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail, PolicyDenyRealtimeGuests, PolicyPasskey, PolicyAccountDevices] API response as one of the typed response models Raises @@ -5647,13 +5903,100 @@ def get_policy( if response.get('$id') == 'passkey': return self._parse_response(response, model=PolicyPasskey) + if response.get('$id') == 'account-devices': + return self._parse_response(response, model=PolicyAccountDevices) + raise AppwriteException('Unable to match response to any known model') + def get_prefs( + self, + model_type: Type[T] = dict, + ) -> Preferences[T]: + """ + Get the project preferences. Preferences are a key-value object stored on the project for anyone working on it. + + Parameters + ---------- + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + + Returns + ------- + Preferences[T] + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/project/prefs' + api_params = {} + + response = self.client.call( + 'get', + api_path, + { + 'X-Appwrite-Project': self.client.get_config('project'), + 'accept': 'application/json', + }, + api_params, + ) + + return Preferences.with_data(response, model_type) + + def update_prefs( + self, + prefs: Dict[str, Any], + model_type: Type[T] = dict, + ) -> Preferences[T]: + """ + Update the project preferences. The object you pass is stored as is and replaces any previous value. The maximum allowed prefs size is 64kB and throws an error if exceeded. + + Parameters + ---------- + prefs : Dict[str, Any] + Prefs key-value JSON object. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + + Returns + ------- + Preferences[T] + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/project/prefs' + api_params = {} + if prefs is None: + raise AppwriteException('Missing required parameter: "prefs"') + api_params['prefs'] = self._normalize_value(prefs) + + 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 Preferences.with_data(response, model_type) + def update_protocol( self, protocol_id: ProjectProtocolId, enabled: bool, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Update properties of a specific protocol. Use this endpoint to enable or disable a protocol in your project. @@ -5663,9 +6006,12 @@ def update_protocol( Protocol name. Can be one of: rest, graphql, websocket enabled : bool Protocol status. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5694,13 +6040,14 @@ def update_protocol( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def update_service( self, service_id: ProjectServiceId, enabled: bool, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Update properties of a specific service. Use this endpoint to enable or disable a service in your project. @@ -5710,9 +6057,12 @@ def update_service( Service name. Can be one of: account, avatars, databases, tablesdb, locale, health, project, storage, teams, users, vcs, sites, functions, proxy, graphql, migrations, messaging, advisor, oauth2 enabled : bool Service status. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5741,7 +6091,7 @@ def update_service( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def delete_sessions( self, @@ -5787,7 +6137,8 @@ def update_smtp( reply_to_name: Optional[str] = None, secure: Optional[ProjectSMTPSecure] = None, enabled: Optional[bool] = None, - ) -> ProjectModel: + model_type: Type[T] = dict, + ) -> ProjectModel[T]: """ Update the SMTP configuration for your project. Use this endpoint to configure your project's SMTP provider with your custom settings for sending transactional emails. @@ -5813,9 +6164,12 @@ def update_smtp( Configures if communication with SMTP server is encrypted. Allowed values are: tls, ssl. Leave empty for no encryption. enabled : Optional[bool] Enable or disable custom SMTP. Custom SMTP is useful for branding purposes, but also allows use of custom email templates. + model_type : Type[T], optional + Pydantic model class for the user-defined data. Defaults to dict for backward compatibility. + Returns ------- - ProjectModel + ProjectModel[T] API response as a typed Pydantic model Raises @@ -5858,7 +6212,7 @@ def update_smtp( api_params, ) - return self._parse_response(response, model=ProjectModel) + return ProjectModel.with_data(response, model_type) def create_smtp_test( self, diff --git a/appwrite/services/proxy.py b/appwrite/services/proxy.py index 273857c5..5aea63fa 100644 --- a/appwrite/services/proxy.py +++ b/appwrite/services/proxy.py @@ -467,12 +467,181 @@ def delete_rule( return response + def update_function_rule( + self, + rule_id: str, + function_id: str, + branch: Optional[str] = None, + ) -> ProxyRule: + """ + Update the function and VCS branch a function proxy rule executes. The rule domain cannot be changed. + + Parameters + ---------- + rule_id : str + Rule ID. + function_id : str + ID of function to be executed. + branch : Optional[str] + Name of VCS branch to deploy changes automatically + Returns + ------- + ProxyRule + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/proxy/rules/{ruleId}/function' + api_params = {} + if rule_id is None or rule_id == '': + raise AppwriteException('Missing required parameter: "rule_id"') + if function_id is None: + raise AppwriteException('Missing required parameter: "function_id"') + api_path = api_path.replace('{ruleId}', self._encode_path(rule_id)) + api_params['functionId'] = self._normalize_value(function_id) + if branch is not None: + api_params['branch'] = self._normalize_value(branch) + + 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=ProxyRule) + + def update_redirect_rule( + self, + rule_id: str, + url: str, + status_code: StatusCode, + resource_id: str, + resource_type: ProxyResourceType, + ) -> ProxyRule: + """ + Update the target URL, status code and parent resource of a redirect proxy rule. The rule domain cannot be changed. + + Parameters + ---------- + rule_id : str + Rule ID. + url : str + Target URL of redirection + status_code : StatusCode + Status code of redirection + resource_id : str + ID of parent resource. + resource_type : ProxyResourceType + Type of parent resource. + Returns + ------- + ProxyRule + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/proxy/rules/{ruleId}/redirect' + api_params = {} + if rule_id is None or rule_id == '': + raise AppwriteException('Missing required parameter: "rule_id"') + if url is None: + raise AppwriteException('Missing required parameter: "url"') + if status_code is None: + raise AppwriteException('Missing required parameter: "status_code"') + if resource_id is None: + raise AppwriteException('Missing required parameter: "resource_id"') + if resource_type is None: + raise AppwriteException('Missing required parameter: "resource_type"') + api_path = api_path.replace('{ruleId}', self._encode_path(rule_id)) + api_params['url'] = self._normalize_value(url) + api_params['statusCode'] = self._normalize_value(status_code) + api_params['resourceId'] = self._normalize_value(resource_id) + api_params['resourceType'] = self._normalize_value(resource_type) + + 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=ProxyRule) + + def update_site_rule( + self, + rule_id: str, + site_id: str, + branch: Optional[str] = None, + ) -> ProxyRule: + """ + Update the site and VCS branch a site proxy rule serves. The rule domain cannot be changed. + + Parameters + ---------- + rule_id : str + Rule ID. + site_id : str + ID of site to be executed. + branch : Optional[str] + Name of VCS branch to deploy changes automatically + Returns + ------- + ProxyRule + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/proxy/rules/{ruleId}/site' + api_params = {} + if rule_id is None or rule_id == '': + raise AppwriteException('Missing required parameter: "rule_id"') + if site_id is None: + raise AppwriteException('Missing required parameter: "site_id"') + api_path = api_path.replace('{ruleId}', self._encode_path(rule_id)) + api_params['siteId'] = self._normalize_value(site_id) + if branch is not None: + api_params['branch'] = self._normalize_value(branch) + + 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=ProxyRule) + def update_rule_status( self, rule_id: str, ) -> ProxyRule: """ - If not succeeded yet, retry verification process of a proxy rule domain. This endpoint triggers domain verification by checking DNS records. If verification is successful, a TLS certificate will be automatically provisioned for the domain asynchronously in the background. + If not succeeded yet, retry verification process of a proxy rule domain. This endpoint triggers domain verification by checking DNS records. If verification is successful, a TLS certificate will be automatically provisioned for the domain asynchronously in the background. A verified rule whose certificate renewal failed or is blocked, or whose certificate expired, is checked the same way, and its certificate renewed in the background. Parameters ---------- diff --git a/appwrite/services/storage.py b/appwrite/services/storage.py index cce353ca..d9415682 100644 --- a/appwrite/services/storage.py +++ b/appwrite/services/storage.py @@ -105,7 +105,7 @@ def create_bucket( compression : Optional[Compression] Compression algorithm chosen for compression. Can be one of none, [gzip](https://en.wikipedia.org/wiki/Gzip), or [zstd](https://en.wikipedia.org/wiki/Zstd), For file size above 20MB compression is skipped even if it's enabled encryption : Optional[bool] - Is encryption enabled? For file size above 20MB encryption is skipped even if it's enabled + Is encryption enabled? Files of any size are encrypted. antivirus : Optional[bool] Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled transformations : Optional[bool] @@ -247,7 +247,7 @@ def update_bucket( compression : Optional[Compression] Compression algorithm chosen for compression. Can be one of none, [gzip](https://en.wikipedia.org/wiki/Gzip), or [zstd](https://en.wikipedia.org/wiki/Zstd), For file size above 20MB compression is skipped even if it's enabled encryption : Optional[bool] - Is encryption enabled? For file size above 20MB encryption is skipped even if it's enabled + Is encryption enabled? Files of any size are encrypted. antivirus : Optional[bool] Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled transformations : Optional[bool] diff --git a/appwrite/services/waf.py b/appwrite/services/waf.py index 3d47de74..18f6275c 100644 --- a/appwrite/services/waf.py +++ b/appwrite/services/waf.py @@ -75,6 +75,7 @@ def create_bypass_rule( priority: Optional[float] = None, enabled: Optional[bool] = None, conditions: Optional[List[str]] = None, + schedule: Optional[Dict[str, Any]] = None, ) -> WafRuleBypass: """ Create a bypass WAF rule. Conditions can match request attributes including `ip` (plain IPs or CIDR blocks like `10.0.0.0/8`), `method`, `path`, `host`, `country`, `continent`, `headers.`, `query.`, `queryKeys`, `accept`, `acceptLanguage`, `cookie`, `userAgent`, `os`, `osVersion`, `browser`, and `browserVersion`. Conditions on `city` and `state` require the premium Geo DB addon. @@ -97,6 +98,8 @@ def create_bypass_rule( Set to false to create the rule in a disabled state. conditions : Optional[List[str]] Array of condition strings generated using the WAF Condition builder. Maximum of 100 conditions are allowed, each 4096 characters long. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime and endTime (HH:MM), and an optional IANA timezone (UTC by default). Leave out for always on; on update, leave out to keep the current schedule and pass an empty object to remove it. Returns ------- WafRuleBypass @@ -129,6 +132,8 @@ def create_bypass_rule( api_params['enabled'] = self._normalize_value(enabled) if conditions is not None: api_params['conditions'] = self._normalize_value(conditions) + if schedule is not None: + api_params['schedule'] = self._normalize_value(schedule) response = self.client.call( 'post', @@ -153,6 +158,7 @@ def update_bypass_rule( priority: Optional[float] = None, enabled: Optional[bool] = None, conditions: Optional[List[str]] = None, + schedule: Optional[Dict[str, Any]] = None, ) -> WafRuleBypass: """ Update a bypass WAF rule. Conditions can match request attributes including `ip` (plain IPs or CIDR blocks like `10.0.0.0/8`), `method`, `path`, `host`, `country`, `continent`, `headers.`, `query.`, `queryKeys`, `accept`, `acceptLanguage`, `cookie`, `userAgent`, `os`, `osVersion`, `browser`, and `browserVersion`. Conditions on `city` and `state` require the premium Geo DB addon. @@ -175,6 +181,8 @@ def update_bypass_rule( Set to false to disable the rule. conditions : Optional[List[str]] Array of condition strings generated using the WAF Condition builder. Maximum of 100 conditions are allowed, each 4096 characters long. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime and endTime (HH:MM), and an optional IANA timezone (UTC by default). Leave out for always on; on update, leave out to keep the current schedule and pass an empty object to remove it. Returns ------- WafRuleBypass @@ -205,6 +213,8 @@ def update_bypass_rule( api_params['enabled'] = self._normalize_value(enabled) if conditions is not None: api_params['conditions'] = self._normalize_value(conditions) + if schedule is not None: + api_params['schedule'] = self._normalize_value(schedule) response = self.client.call( 'patch', @@ -232,6 +242,7 @@ def create_challenge_rule( conditions: Optional[List[str]] = None, difficulty: Optional[float] = None, ttl: Optional[float] = None, + schedule: Optional[Dict[str, Any]] = None, ) -> WafRuleChallenge: """ Create a challenge WAF rule. Use `difficulty` (1 easiest to 5 hardest) to tune the client-side proof-of-work cost, and `ttl` to control how long, in seconds, a visitor stays cleared after passing the challenge before being challenged again. Conditions can match request attributes including `ip` (plain IPs or CIDR blocks like `10.0.0.0/8`), `method`, `path`, `host`, `country`, `continent`, `headers.`, `query.`, `queryKeys`, `accept`, `acceptLanguage`, `cookie`, `userAgent`, `os`, `osVersion`, `browser`, and `browserVersion`. Conditions on `city` and `state` require the premium Geo DB addon. @@ -260,6 +271,8 @@ def create_challenge_rule( Challenge difficulty from 1 (easiest) to 5 (hardest). Higher values demand more client-side proof-of-work. ttl : Optional[float] How long, in seconds, a visitor stays cleared after passing the challenge before being challenged again. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime and endTime (HH:MM), and an optional IANA timezone (UTC by default). Leave out for always on; on update, leave out to keep the current schedule and pass an empty object to remove it. Returns ------- WafRuleChallenge @@ -298,6 +311,8 @@ def create_challenge_rule( api_params['difficulty'] = self._normalize_value(difficulty) if ttl is not None: api_params['ttl'] = self._normalize_value(ttl) + if schedule is not None: + api_params['schedule'] = self._normalize_value(schedule) response = self.client.call( 'post', @@ -325,6 +340,7 @@ def update_challenge_rule( conditions: Optional[List[str]] = None, difficulty: Optional[float] = None, ttl: Optional[float] = None, + schedule: Optional[Dict[str, Any]] = None, ) -> WafRuleChallenge: """ Update a challenge WAF rule. Use `difficulty` (1 easiest to 5 hardest) to tune the client-side proof-of-work cost, and `ttl` to control how long, in seconds, a visitor stays cleared after passing the challenge before being challenged again. Conditions can match request attributes including `ip` (plain IPs or CIDR blocks like `10.0.0.0/8`), `method`, `path`, `host`, `country`, `continent`, `headers.`, `query.`, `queryKeys`, `accept`, `acceptLanguage`, `cookie`, `userAgent`, `os`, `osVersion`, `browser`, and `browserVersion`. Conditions on `city` and `state` require the premium Geo DB addon. @@ -353,6 +369,8 @@ def update_challenge_rule( Challenge difficulty from 1 (easiest) to 5 (hardest). Higher values demand more client-side proof-of-work. ttl : Optional[float] How long, in seconds, a visitor stays cleared after passing the challenge before being challenged again. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime and endTime (HH:MM), and an optional IANA timezone (UTC by default). Leave out for always on; on update, leave out to keep the current schedule and pass an empty object to remove it. Returns ------- WafRuleChallenge @@ -389,6 +407,8 @@ def update_challenge_rule( api_params['difficulty'] = self._normalize_value(difficulty) if ttl is not None: api_params['ttl'] = self._normalize_value(ttl) + if schedule is not None: + api_params['schedule'] = self._normalize_value(schedule) response = self.client.call( 'patch', @@ -413,6 +433,7 @@ def create_deny_rule( priority: Optional[float] = None, enabled: Optional[bool] = None, conditions: Optional[List[str]] = None, + schedule: Optional[Dict[str, Any]] = None, ) -> WafRuleDeny: """ Create a deny WAF rule. Conditions can match request attributes including `ip` (plain IPs or CIDR blocks like `10.0.0.0/8`), `method`, `path`, `host`, `country`, `continent`, `headers.`, `query.`, `queryKeys`, `accept`, `acceptLanguage`, `cookie`, `userAgent`, `os`, `osVersion`, `browser`, and `browserVersion`. Conditions on `city` and `state` require the premium Geo DB addon. `sessionAge` and `mfaAge` hold the seconds since the request's session was created and since its last MFA challenge. Both are null for requests without a session, such as guests and API keys, while a request authenticated with the `x-appwrite-jwt` header carries the age of the session its token is bound to; on `functions` rules the session comes from the `x-appwrite-user-jwt` header, and `mfaAge` is null when the session never completed MFA. A deny matching on `mfaAge` returns `user_challenge_required` and one matching on `sessionAge` returns `user_reauthentication_required`, both with status 401, so the user can sign in again and retry. To also deny requests without a session, combine `isNull` and `greaterThan` in an `or` condition. @@ -435,6 +456,8 @@ def create_deny_rule( Set to false to create the rule in a disabled state. conditions : Optional[List[str]] Array of condition strings generated using the WAF Condition builder. Maximum of 100 conditions are allowed, each 4096 characters long. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime and endTime (HH:MM), and an optional IANA timezone (UTC by default). Leave out for always on; on update, leave out to keep the current schedule and pass an empty object to remove it. Returns ------- WafRuleDeny @@ -467,6 +490,8 @@ def create_deny_rule( api_params['enabled'] = self._normalize_value(enabled) if conditions is not None: api_params['conditions'] = self._normalize_value(conditions) + if schedule is not None: + api_params['schedule'] = self._normalize_value(schedule) response = self.client.call( 'post', @@ -491,6 +516,7 @@ def update_deny_rule( priority: Optional[float] = None, enabled: Optional[bool] = None, conditions: Optional[List[str]] = None, + schedule: Optional[Dict[str, Any]] = None, ) -> WafRuleDeny: """ Update a deny WAF rule. Conditions can match request attributes including `ip` (plain IPs or CIDR blocks like `10.0.0.0/8`), `method`, `path`, `host`, `country`, `continent`, `headers.`, `query.`, `queryKeys`, `accept`, `acceptLanguage`, `cookie`, `userAgent`, `os`, `osVersion`, `browser`, and `browserVersion`. Conditions on `city` and `state` require the premium Geo DB addon. `sessionAge` and `mfaAge` hold the seconds since the request's session was created and since its last MFA challenge. Both are null for requests without a session, such as guests and API keys, while a request authenticated with the `x-appwrite-jwt` header carries the age of the session its token is bound to; on `functions` rules the session comes from the `x-appwrite-user-jwt` header, and `mfaAge` is null when the session never completed MFA. A deny matching on `mfaAge` returns `user_challenge_required` and one matching on `sessionAge` returns `user_reauthentication_required`, both with status 401, so the user can sign in again and retry. To also deny requests without a session, combine `isNull` and `greaterThan` in an `or` condition. @@ -513,6 +539,8 @@ def update_deny_rule( Set to false to disable the rule. conditions : Optional[List[str]] Array of condition strings generated using the WAF Condition builder. Maximum of 100 conditions are allowed, each 4096 characters long. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime and endTime (HH:MM), and an optional IANA timezone (UTC by default). Leave out for always on; on update, leave out to keep the current schedule and pass an empty object to remove it. Returns ------- WafRuleDeny @@ -543,6 +571,8 @@ def update_deny_rule( api_params['enabled'] = self._normalize_value(enabled) if conditions is not None: api_params['conditions'] = self._normalize_value(conditions) + if schedule is not None: + api_params['schedule'] = self._normalize_value(schedule) response = self.client.call( 'patch', @@ -572,6 +602,7 @@ def create_rate_limit_rule( priority: Optional[float] = None, enabled: Optional[bool] = None, conditions: Optional[List[str]] = None, + schedule: Optional[Dict[str, Any]] = None, ) -> WafRuleRateLimit: """ Create a rate limit WAF rule. Use `key` to choose the counter: `ip` limits per client IP, while `userId` limits per authenticated user (requests without an authenticated user skip `userId` rules). Conditions can match request attributes including `ip` (plain IPs or CIDR blocks like `10.0.0.0/8`), `method`, `path`, `host`, `country`, `continent`, `headers.`, `query.`, `queryKeys`, `accept`, `acceptLanguage`, `cookie`, `userAgent`, `os`, `osVersion`, `browser`, and `browserVersion`. Conditions on `city` and `state` require the premium Geo DB addon. @@ -604,6 +635,8 @@ def create_rate_limit_rule( Set to false to create the rule in a disabled state. conditions : Optional[List[str]] Array of condition strings generated using the WAF Condition builder. Maximum of 100 conditions are allowed, each 4096 characters long. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime and endTime (HH:MM), and an optional IANA timezone (UTC by default). Leave out for always on; on update, leave out to keep the current schedule and pass an empty object to remove it. Returns ------- WafRuleRateLimit @@ -648,6 +681,8 @@ def create_rate_limit_rule( api_params['enabled'] = self._normalize_value(enabled) if conditions is not None: api_params['conditions'] = self._normalize_value(conditions) + if schedule is not None: + api_params['schedule'] = self._normalize_value(schedule) response = self.client.call( 'post', @@ -676,6 +711,7 @@ def update_rate_limit_rule( priority: Optional[float] = None, enabled: Optional[bool] = None, conditions: Optional[List[str]] = None, + schedule: Optional[Dict[str, Any]] = None, ) -> WafRuleRateLimit: """ Update a rate limit WAF rule. Use `key` to choose the counter: `ip` limits per client IP, while `userId` limits per authenticated user (requests without an authenticated user skip `userId` rules). Conditions can match request attributes including `ip` (plain IPs or CIDR blocks like `10.0.0.0/8`), `method`, `path`, `host`, `country`, `continent`, `headers.`, `query.`, `queryKeys`, `accept`, `acceptLanguage`, `cookie`, `userAgent`, `os`, `osVersion`, `browser`, and `browserVersion`. Conditions on `city` and `state` require the premium Geo DB addon. @@ -706,6 +742,8 @@ def update_rate_limit_rule( Set to false to disable the rule. conditions : Optional[List[str]] Array of condition strings generated using the WAF Condition builder. Maximum of 100 conditions are allowed, each 4096 characters long. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime and endTime (HH:MM), and an optional IANA timezone (UTC by default). Leave out for always on; on update, leave out to keep the current schedule and pass an empty object to remove it. Returns ------- WafRuleRateLimit @@ -744,6 +782,8 @@ def update_rate_limit_rule( api_params['enabled'] = self._normalize_value(enabled) if conditions is not None: api_params['conditions'] = self._normalize_value(conditions) + if schedule is not None: + api_params['schedule'] = self._normalize_value(schedule) response = self.client.call( 'patch', @@ -770,6 +810,7 @@ def create_redirect_rule( priority: Optional[float] = None, enabled: Optional[bool] = None, conditions: Optional[List[str]] = None, + schedule: Optional[Dict[str, Any]] = None, ) -> WafRuleRedirect: """ Create a redirect WAF rule. Conditions can match request attributes including `ip` (plain IPs or CIDR blocks like `10.0.0.0/8`), `method`, `path`, `host`, `country`, `continent`, `headers.`, `query.`, `queryKeys`, `accept`, `acceptLanguage`, `cookie`, `userAgent`, `os`, `osVersion`, `browser`, and `browserVersion`. Conditions on `city` and `state` require the premium Geo DB addon. @@ -796,6 +837,8 @@ def create_redirect_rule( Set to false to create the rule in a disabled state. conditions : Optional[List[str]] Array of condition strings generated using the WAF Condition builder. Maximum of 100 conditions are allowed, each 4096 characters long. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime and endTime (HH:MM), and an optional IANA timezone (UTC by default). Leave out for always on; on update, leave out to keep the current schedule and pass an empty object to remove it. Returns ------- WafRuleRedirect @@ -834,6 +877,8 @@ def create_redirect_rule( api_params['enabled'] = self._normalize_value(enabled) if conditions is not None: api_params['conditions'] = self._normalize_value(conditions) + if schedule is not None: + api_params['schedule'] = self._normalize_value(schedule) response = self.client.call( 'post', @@ -860,6 +905,7 @@ def update_redirect_rule( priority: Optional[float] = None, enabled: Optional[bool] = None, conditions: Optional[List[str]] = None, + schedule: Optional[Dict[str, Any]] = None, ) -> WafRuleRedirect: """ Update a redirect WAF rule. Conditions can match request attributes including `ip` (plain IPs or CIDR blocks like `10.0.0.0/8`), `method`, `path`, `host`, `country`, `continent`, `headers.`, `query.`, `queryKeys`, `accept`, `acceptLanguage`, `cookie`, `userAgent`, `os`, `osVersion`, `browser`, and `browserVersion`. Conditions on `city` and `state` require the premium Geo DB addon. @@ -886,6 +932,8 @@ def update_redirect_rule( Set to false to disable the rule. conditions : Optional[List[str]] Array of condition strings generated using the WAF Condition builder. Maximum of 100 conditions are allowed, each 4096 characters long. + schedule : Optional[Dict[str, Any]] + When the rule is active: mode, days, interval, anchor, startTime and endTime (HH:MM), and an optional IANA timezone (UTC by default). Leave out for always on; on update, leave out to keep the current schedule and pass an empty object to remove it. Returns ------- WafRuleRedirect @@ -920,6 +968,8 @@ def update_redirect_rule( api_params['enabled'] = self._normalize_value(enabled) if conditions is not None: api_params['conditions'] = self._normalize_value(conditions) + if schedule is not None: + api_params['schedule'] = self._normalize_value(schedule) response = self.client.call( 'patch', diff --git a/docs/examples/account/create-device.md b/docs/examples/account/create-device.md new file mode 100644 index 00000000..c94577b0 --- /dev/null +++ b/docs/examples/account/create-device.md @@ -0,0 +1,16 @@ +```python +from appwrite.client import Client +from appwrite.services.account import Account +from appwrite.models import Device + +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: Device = account.create_device() + +print(result.model_dump()) +``` diff --git a/docs/examples/account/delete-device.md b/docs/examples/account/delete-device.md new file mode 100644 index 00000000..18e0e31f --- /dev/null +++ b/docs/examples/account/delete-device.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_device( + device_id = '' +) +``` diff --git a/docs/examples/account/get-device.md b/docs/examples/account/get-device.md new file mode 100644 index 00000000..1a79a81b --- /dev/null +++ b/docs/examples/account/get-device.md @@ -0,0 +1,18 @@ +```python +from appwrite.client import Client +from appwrite.services.account import Account +from appwrite.models import Device + +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: Device = account.get_device( + device_id = '' +) + +print(result.model_dump()) +``` diff --git a/docs/examples/account/update-device-session.md b/docs/examples/account/update-device-session.md new file mode 100644 index 00000000..9d00dd91 --- /dev/null +++ b/docs/examples/account/update-device-session.md @@ -0,0 +1,19 @@ +```python +from appwrite.client import Client +from appwrite.services.account import Account +from appwrite.models import Session + +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: Session = account.update_device_session( + device_id = '', + session_id = '' +) + +print(result.model_dump()) +``` diff --git a/docs/examples/project/create-environment.md b/docs/examples/project/create-environment.md index cf7cd504..b40d8e4e 100644 --- a/docs/examples/project/create-environment.md +++ b/docs/examples/project/create-environment.md @@ -1,7 +1,7 @@ ```python from appwrite.client import Client from appwrite.services.project import Project -from appwrite.models import Project as ProjectModel +from appwrite.models import Environment from appwrite.enums.environment_resource import EnvironmentResource client = Client() @@ -11,7 +11,7 @@ client.set_key('') # Your secret API key project = Project(client) -result: ProjectModel = project.create_environment( +result: Environment = project.create_environment( environment_id = '', name = '', resources = [EnvironmentResource.DATABASE, EnvironmentResource.TABLE, EnvironmentResource.COLUMN, EnvironmentResource.INDEX, EnvironmentResource.BUCKET, EnvironmentResource.TEAM, EnvironmentResource.FUNCTION, EnvironmentResource.ENVIRONMENT_VARIABLE] # optional diff --git a/docs/examples/project/get-environment.md b/docs/examples/project/get-environment.md index 82082dfb..079126bb 100644 --- a/docs/examples/project/get-environment.md +++ b/docs/examples/project/get-environment.md @@ -1,7 +1,7 @@ ```python from appwrite.client import Client from appwrite.services.project import Project -from appwrite.models import Project as ProjectModel +from appwrite.models import Environment client = Client() client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint @@ -10,7 +10,7 @@ client.set_key('') # Your secret API key project = Project(client) -result: ProjectModel = project.get_environment( +result: Environment = project.get_environment( environment_id = '' ) diff --git a/docs/examples/project/get-o-auth-2-provider.md b/docs/examples/project/get-o-auth-2-provider.md index 55a01469..ca5387ff 100644 --- a/docs/examples/project/get-o-auth-2-provider.md +++ b/docs/examples/project/get-o-auth-2-provider.md @@ -47,6 +47,7 @@ from appwrite.models import OAuth2Kick from appwrite.models import OAuth2Microsoft from appwrite.models import OAuth2TikTok from appwrite.models import OAuth2Kakao +from appwrite.models import OAuth2Vipps from typing import Union from appwrite.enums.project_o_auth_provider_id import ProjectOAuthProviderId @@ -57,7 +58,7 @@ client.set_key('') # Your secret API key project = Project(client) -result: Union[OAuth2Github, OAuth2Discord, OAuth2Figma, OAuth2Dropbox, OAuth2Dailymotion, OAuth2Bitbucket, OAuth2Bitly, OAuth2Box, OAuth2Autodesk, OAuth2Google, OAuth2Zoom, OAuth2Zoho, OAuth2Yandex, OAuth2X, OAuth2WordPress, OAuth2Webflow, OAuth2Twitch, OAuth2Stripe, OAuth2Spotify, OAuth2Slack, OAuth2Podio, OAuth2Notion, OAuth2Salesforce, OAuth2Yahoo, OAuth2HuggingFace, OAuth2Resend, OAuth2Cloudflare, OAuth2Linkedin, OAuth2Disqus, OAuth2Amazon, OAuth2Etsy, OAuth2Facebook, OAuth2Tradeshift, OAuth2Paypal, OAuth2Gitlab, OAuth2Authentik, OAuth2Auth0, OAuth2FusionAuth, OAuth2Keycloak, OAuth2Oidc, OAuth2Apple, OAuth2Okta, OAuth2Kick, OAuth2Microsoft, OAuth2TikTok, OAuth2Kakao] = project.get_o_auth2_provider( +result: Union[OAuth2Github, OAuth2Discord, OAuth2Figma, OAuth2Dropbox, OAuth2Dailymotion, OAuth2Bitbucket, OAuth2Bitly, OAuth2Box, OAuth2Autodesk, OAuth2Google, OAuth2Zoom, OAuth2Zoho, OAuth2Yandex, OAuth2X, OAuth2WordPress, OAuth2Webflow, OAuth2Twitch, OAuth2Stripe, OAuth2Spotify, OAuth2Slack, OAuth2Podio, OAuth2Notion, OAuth2Salesforce, OAuth2Yahoo, OAuth2HuggingFace, OAuth2Resend, OAuth2Cloudflare, OAuth2Linkedin, OAuth2Disqus, OAuth2Amazon, OAuth2Etsy, OAuth2Facebook, OAuth2Tradeshift, OAuth2Paypal, OAuth2Gitlab, OAuth2Authentik, OAuth2Auth0, OAuth2FusionAuth, OAuth2Keycloak, OAuth2Oidc, OAuth2Apple, OAuth2Okta, OAuth2Kick, OAuth2Microsoft, OAuth2TikTok, OAuth2Kakao, OAuth2Vipps] = project.get_o_auth2_provider( provider_id = ProjectOAuthProviderId.AMAZON ) diff --git a/docs/examples/project/get-policy.md b/docs/examples/project/get-policy.md index ec56a786..2d123683 100644 --- a/docs/examples/project/get-policy.md +++ b/docs/examples/project/get-policy.md @@ -20,6 +20,7 @@ from appwrite.models import PolicyDenyFreeEmail from appwrite.models import PolicyDenyCorporateEmail from appwrite.models import PolicyDenyRealtimeGuests from appwrite.models import PolicyPasskey +from appwrite.models import PolicyAccountDevices from typing import Union from appwrite.enums.project_policy_id import ProjectPolicyId @@ -30,7 +31,7 @@ client.set_key('') # Your secret API key project = Project(client) -result: Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicyPasswordPwned, PolicyPasswordRotation, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyMfaFactors, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail, PolicyDenyRealtimeGuests, PolicyPasskey] = project.get_policy( +result: Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicyPasswordPwned, PolicyPasswordRotation, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyMfaFactors, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail, PolicyDenyRealtimeGuests, PolicyPasskey, PolicyAccountDevices] = project.get_policy( policy_id = ProjectPolicyId.PASSWORD_DICTIONARY ) diff --git a/docs/examples/project/get-prefs.md b/docs/examples/project/get-prefs.md new file mode 100644 index 00000000..bfe732a4 --- /dev/null +++ b/docs/examples/project/get-prefs.md @@ -0,0 +1,16 @@ +```python +from appwrite.client import Client +from appwrite.services.project import Project +from appwrite.models import Preferences + +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: Preferences = project.get_prefs() + +print(result.model_dump()) +``` diff --git a/docs/examples/project/list-environments.md b/docs/examples/project/list-environments.md index f414fbea..a5deb576 100644 --- a/docs/examples/project/list-environments.md +++ b/docs/examples/project/list-environments.md @@ -1,7 +1,7 @@ ```python from appwrite.client import Client from appwrite.services.project import Project -from appwrite.models import ProjectList +from appwrite.models import EnvironmentList client = Client() client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint @@ -10,7 +10,7 @@ client.set_key('') # Your secret API key project = Project(client) -result: ProjectList = project.list_environments( +result: EnvironmentList = project.list_environments( queries = [], # optional total = False # optional ) diff --git a/docs/examples/project/update-account-devices-policy.md b/docs/examples/project/update-account-devices-policy.md new file mode 100644 index 00000000..fa0724f7 --- /dev/null +++ b/docs/examples/project/update-account-devices-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_account_devices_policy( + enabled = False +) + +print(result.model_dump()) +``` diff --git a/docs/examples/project/update-environment.md b/docs/examples/project/update-environment.md index e4db2999..47f738fc 100644 --- a/docs/examples/project/update-environment.md +++ b/docs/examples/project/update-environment.md @@ -1,7 +1,7 @@ ```python from appwrite.client import Client from appwrite.services.project import Project -from appwrite.models import Project as ProjectModel +from appwrite.models import Environment client = Client() client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint @@ -10,7 +10,7 @@ client.set_key('') # Your secret API key project = Project(client) -result: ProjectModel = project.update_environment( +result: Environment = project.update_environment( environment_id = '', name = '' ) diff --git a/docs/examples/project/update-o-auth-2-vipps-sandbox.md b/docs/examples/project/update-o-auth-2-vipps-sandbox.md new file mode 100644 index 00000000..88a72a7b --- /dev/null +++ b/docs/examples/project/update-o-auth-2-vipps-sandbox.md @@ -0,0 +1,20 @@ +```python +from appwrite.client import Client +from appwrite.services.project import Project +from appwrite.models import OAuth2Vipps + +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: OAuth2Vipps = project.update_o_auth2_vipps_sandbox( + client_id = '', # optional + client_secret = '', # optional + enabled = False # optional +) + +print(result.model_dump()) +``` diff --git a/docs/examples/project/update-o-auth-2-vipps.md b/docs/examples/project/update-o-auth-2-vipps.md new file mode 100644 index 00000000..e0657103 --- /dev/null +++ b/docs/examples/project/update-o-auth-2-vipps.md @@ -0,0 +1,20 @@ +```python +from appwrite.client import Client +from appwrite.services.project import Project +from appwrite.models import OAuth2Vipps + +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: OAuth2Vipps = project.update_o_auth2_vipps( + client_id = '', # optional + client_secret = '', # optional + enabled = False # optional +) + +print(result.model_dump()) +``` diff --git a/docs/examples/project/update-prefs.md b/docs/examples/project/update-prefs.md new file mode 100644 index 00000000..a543bd03 --- /dev/null +++ b/docs/examples/project/update-prefs.md @@ -0,0 +1,18 @@ +```python +from appwrite.client import Client +from appwrite.services.project import Project +from appwrite.models import Preferences + +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: Preferences = project.update_prefs( + prefs = {} +) + +print(result.model_dump()) +``` diff --git a/docs/examples/proxy/update-function-rule.md b/docs/examples/proxy/update-function-rule.md new file mode 100644 index 00000000..0b82650d --- /dev/null +++ b/docs/examples/proxy/update-function-rule.md @@ -0,0 +1,20 @@ +```python +from appwrite.client import Client +from appwrite.services.proxy import Proxy +from appwrite.models import ProxyRule + +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 + +proxy = Proxy(client) + +result: ProxyRule = proxy.update_function_rule( + rule_id = '', + function_id = '', + branch = '' # optional +) + +print(result.model_dump()) +``` diff --git a/docs/examples/proxy/update-redirect-rule.md b/docs/examples/proxy/update-redirect-rule.md new file mode 100644 index 00000000..19877feb --- /dev/null +++ b/docs/examples/proxy/update-redirect-rule.md @@ -0,0 +1,24 @@ +```python +from appwrite.client import Client +from appwrite.services.proxy import Proxy +from appwrite.models import ProxyRule +from appwrite.enums.status_code import StatusCode +from appwrite.enums.proxy_resource_type import ProxyResourceType + +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 + +proxy = Proxy(client) + +result: ProxyRule = proxy.update_redirect_rule( + rule_id = '', + url = 'https://example.com', + status_code = StatusCode.MOVEDPERMANENTLY, + resource_id = '', + resource_type = ProxyResourceType.SITE +) + +print(result.model_dump()) +``` diff --git a/docs/examples/proxy/update-site-rule.md b/docs/examples/proxy/update-site-rule.md new file mode 100644 index 00000000..a6121cca --- /dev/null +++ b/docs/examples/proxy/update-site-rule.md @@ -0,0 +1,20 @@ +```python +from appwrite.client import Client +from appwrite.services.proxy import Proxy +from appwrite.models import ProxyRule + +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 + +proxy = Proxy(client) + +result: ProxyRule = proxy.update_site_rule( + rule_id = '', + site_id = '', + branch = '' # optional +) + +print(result.model_dump()) +``` diff --git a/docs/examples/waf/create-bypass-rule.md b/docs/examples/waf/create-bypass-rule.md index 70ed9061..72be3374 100644 --- a/docs/examples/waf/create-bypass-rule.md +++ b/docs/examples/waf/create-bypass-rule.md @@ -18,7 +18,8 @@ result: WafRuleBypass = waf.create_bypass_rule( description = '', # optional priority = -100000, # optional enabled = False, # optional - conditions = [] # optional + conditions = [], # optional + schedule = {} # optional ) print(result.model_dump()) diff --git a/docs/examples/waf/create-challenge-rule.md b/docs/examples/waf/create-challenge-rule.md index e3d2fea2..29b93d66 100644 --- a/docs/examples/waf/create-challenge-rule.md +++ b/docs/examples/waf/create-challenge-rule.md @@ -21,7 +21,8 @@ result: WafRuleChallenge = waf.create_challenge_rule( enabled = False, # optional conditions = [], # optional difficulty = 1, # optional - ttl = 900 # optional + ttl = 900, # optional + schedule = {} # optional ) print(result.model_dump()) diff --git a/docs/examples/waf/create-deny-rule.md b/docs/examples/waf/create-deny-rule.md index a23ae249..bddbba35 100644 --- a/docs/examples/waf/create-deny-rule.md +++ b/docs/examples/waf/create-deny-rule.md @@ -18,7 +18,8 @@ result: WafRuleDeny = waf.create_deny_rule( description = '', # optional priority = -100000, # optional enabled = False, # optional - conditions = [] # optional + conditions = [], # optional + schedule = {} # optional ) print(result.model_dump()) diff --git a/docs/examples/waf/create-rate-limit-rule.md b/docs/examples/waf/create-rate-limit-rule.md index e76605f8..9e9738f7 100644 --- a/docs/examples/waf/create-rate-limit-rule.md +++ b/docs/examples/waf/create-rate-limit-rule.md @@ -23,7 +23,8 @@ result: WafRuleRateLimit = waf.create_rate_limit_rule( max_bucket_size = 1, # optional priority = -100000, # optional enabled = False, # optional - conditions = [] # optional + conditions = [], # optional + schedule = {} # optional ) print(result.model_dump()) diff --git a/docs/examples/waf/create-redirect-rule.md b/docs/examples/waf/create-redirect-rule.md index a33a1e6c..dc25b1db 100644 --- a/docs/examples/waf/create-redirect-rule.md +++ b/docs/examples/waf/create-redirect-rule.md @@ -20,7 +20,8 @@ result: WafRuleRedirect = waf.create_redirect_rule( description = '', # optional priority = -100000, # optional enabled = False, # optional - conditions = [] # optional + conditions = [], # optional + schedule = {} # optional ) print(result.model_dump()) diff --git a/docs/examples/waf/update-bypass-rule.md b/docs/examples/waf/update-bypass-rule.md index 39ceb7f4..3e604c82 100644 --- a/docs/examples/waf/update-bypass-rule.md +++ b/docs/examples/waf/update-bypass-rule.md @@ -18,7 +18,8 @@ result: WafRuleBypass = waf.update_bypass_rule( description = '', # optional priority = -100000, # optional enabled = False, # optional - conditions = [] # optional + conditions = [], # optional + schedule = {} # optional ) print(result.model_dump()) diff --git a/docs/examples/waf/update-challenge-rule.md b/docs/examples/waf/update-challenge-rule.md index 30afcd7f..f3e2219c 100644 --- a/docs/examples/waf/update-challenge-rule.md +++ b/docs/examples/waf/update-challenge-rule.md @@ -21,7 +21,8 @@ result: WafRuleChallenge = waf.update_challenge_rule( enabled = False, # optional conditions = [], # optional difficulty = 1, # optional - ttl = 900 # optional + ttl = 900, # optional + schedule = {} # optional ) print(result.model_dump()) diff --git a/docs/examples/waf/update-deny-rule.md b/docs/examples/waf/update-deny-rule.md index 2253e2b7..e370a4e0 100644 --- a/docs/examples/waf/update-deny-rule.md +++ b/docs/examples/waf/update-deny-rule.md @@ -18,7 +18,8 @@ result: WafRuleDeny = waf.update_deny_rule( description = '', # optional priority = -100000, # optional enabled = False, # optional - conditions = [] # optional + conditions = [], # optional + schedule = {} # optional ) print(result.model_dump()) diff --git a/docs/examples/waf/update-rate-limit-rule.md b/docs/examples/waf/update-rate-limit-rule.md index e413cef8..5473e6a0 100644 --- a/docs/examples/waf/update-rate-limit-rule.md +++ b/docs/examples/waf/update-rate-limit-rule.md @@ -22,7 +22,8 @@ result: WafRuleRateLimit = waf.update_rate_limit_rule( max_bucket_size = 1, # optional priority = -100000, # optional enabled = False, # optional - conditions = [] # optional + conditions = [], # optional + schedule = {} # optional ) print(result.model_dump()) diff --git a/docs/examples/waf/update-redirect-rule.md b/docs/examples/waf/update-redirect-rule.md index 7e294f8c..b195faf3 100644 --- a/docs/examples/waf/update-redirect-rule.md +++ b/docs/examples/waf/update-redirect-rule.md @@ -20,7 +20,8 @@ result: WafRuleRedirect = waf.update_redirect_rule( status_code = 300, # optional priority = -100000, # optional enabled = False, # optional - conditions = [] # optional + conditions = [], # optional + schedule = {} # optional ) print(result.model_dump()) diff --git a/pyproject.toml b/pyproject.toml index 910edbea..106f5d3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "appwrite" -version = "27.1.0" +version = "27.2.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.10" diff --git a/setup.py b/setup.py index 6d48fc4a..6b0ee0e6 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setuptools.setup( name='appwrite', packages=setuptools.find_packages(), - version='27.1.0', + version='27.2.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/27.1.0.tar.gz', + download_url='https://github.com/appwrite/sdk-for-python/archive/27.2.0.tar.gz', install_requires=[ 'requests>=2.34.2,<3', 'urllib3>=2.8.0,<3', diff --git a/test/services/test_account.py b/test/services/test_account.py index 4e816a23..e6965ad1 100644 --- a/test/services/test_account.py +++ b/test/services/test_account.py @@ -287,6 +287,149 @@ def test_delete_consent_token(self, m): self.assertEqual(sent, {}) self.assertEqual(response, data) + @requests_mock.Mocker() + def test_create_device(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "expire": "2020-10-15T06:38:00.000+00:00", + "activeSessionId": "5e5bb8c16897e", + "sessions": [], + "secret": "5e5bb8c16897e", + } + headers = {'Content-Type': 'application/json'} + m.request( + 'POST', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.create_device() + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/account/devices', + ) + sent = json.loads(request.body or '{}') + self.assertEqual(sent, {}) + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_get_device(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "expire": "2020-10-15T06:38:00.000+00:00", + "activeSessionId": "5e5bb8c16897e", + "sessions": [], + "secret": "5e5bb8c16897e", + } + headers = {'Content-Type': 'application/json'} + m.request( + 'GET', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.get_device( + '', + ) + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/account/devices/', + ) + sent = parse_qs(url.query, keep_blank_values=True) + self.assertEqual(sent, {}) + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_delete_device(self, m): + data = '' + headers = {'Content-Type': 'application/json'} + m.request( + 'DELETE', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.delete_device( + '', + ) + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/account/devices/', + ) + sent = json.loads(request.body or '{}') + self.assertEqual(sent, {}) + self.assertEqual(response, data) + + @requests_mock.Mocker() + def test_update_device_session(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "userId": "5e5bb8c16897e", + "userName": "John Doe", + "expire": "2020-10-15T06:38:00.000+00:00", + "provider": "email", + "providerUid": "user@example.com", + "providerAccessToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", + "providerAccessTokenExpiry": "2020-10-15T06:38:00.000+00:00", + "providerRefreshToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", + "ip": "127.0.0.1", + "osCode": "Mac", + "osName": "Mac", + "osVersion": "Mac", + "clientType": "browser", + "clientCode": "CM", + "clientName": "Chrome Mobile iOS", + "clientVersion": "84.0", + "clientEngine": "WebKit", + "clientEngineVersion": "605.1.15", + "deviceName": "smartphone", + "deviceBrand": "Google", + "deviceModel": "Nexus 5", + "countryCode": "US", + "countryName": "United States", + "current": True, + "factors": [], + "secret": "5e5bb8c16897e", + "mfaUpdatedAt": "2020-10-15T06:38:00.000+00:00", + } + headers = {'Content-Type': 'application/json'} + m.request( + 'PATCH', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.account.update_device_session( + '', + '', + ) + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/account/devices//session', + ) + sent = json.loads(request.body or '{}') + self.assertEqual( + sent, + { + 'sessionId': '', + }, + ) + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_update_email(self, m): data = { @@ -729,6 +872,7 @@ def test_update_mfa_challenge(self, m): "$createdAt": "2020-10-15T06:38:00.000+00:00", "$updatedAt": "2020-10-15T06:38:00.000+00:00", "userId": "5e5bb8c16897e", + "userName": "John Doe", "expire": "2020-10-15T06:38:00.000+00:00", "provider": "email", "providerUid": "user@example.com", @@ -789,6 +933,7 @@ def test_update_mfa_challenge(self, m): "$createdAt": "2020-10-15T06:38:00.000+00:00", "$updatedAt": "2020-10-15T06:38:00.000+00:00", "userId": "5e5bb8c16897e", + "userName": "John Doe", "expire": "2020-10-15T06:38:00.000+00:00", "provider": "email", "providerUid": "user@example.com", @@ -1647,6 +1792,7 @@ def test_create_anonymous_session(self, m): "$createdAt": "2020-10-15T06:38:00.000+00:00", "$updatedAt": "2020-10-15T06:38:00.000+00:00", "userId": "5e5bb8c16897e", + "userName": "John Doe", "expire": "2020-10-15T06:38:00.000+00:00", "provider": "email", "providerUid": "user@example.com", @@ -1698,6 +1844,7 @@ def test_create_email_password_session(self, m): "$createdAt": "2020-10-15T06:38:00.000+00:00", "$updatedAt": "2020-10-15T06:38:00.000+00:00", "userId": "5e5bb8c16897e", + "userName": "John Doe", "expire": "2020-10-15T06:38:00.000+00:00", "provider": "email", "providerUid": "user@example.com", @@ -1760,6 +1907,7 @@ def test_create_id_token_session(self, m): "$createdAt": "2020-10-15T06:38:00.000+00:00", "$updatedAt": "2020-10-15T06:38:00.000+00:00", "userId": "5e5bb8c16897e", + "userName": "John Doe", "expire": "2020-10-15T06:38:00.000+00:00", "provider": "email", "providerUid": "user@example.com", @@ -1828,6 +1976,7 @@ def test_update_magic_url_session(self, m): "$createdAt": "2020-10-15T06:38:00.000+00:00", "$updatedAt": "2020-10-15T06:38:00.000+00:00", "userId": "5e5bb8c16897e", + "userName": "John Doe", "expire": "2020-10-15T06:38:00.000+00:00", "provider": "email", "providerUid": "user@example.com", @@ -1888,6 +2037,7 @@ def test_update_phone_session(self, m): "$createdAt": "2020-10-15T06:38:00.000+00:00", "$updatedAt": "2020-10-15T06:38:00.000+00:00", "userId": "5e5bb8c16897e", + "userName": "John Doe", "expire": "2020-10-15T06:38:00.000+00:00", "provider": "email", "providerUid": "user@example.com", @@ -1948,6 +2098,7 @@ def test_create_session(self, m): "$createdAt": "2020-10-15T06:38:00.000+00:00", "$updatedAt": "2020-10-15T06:38:00.000+00:00", "userId": "5e5bb8c16897e", + "userName": "John Doe", "expire": "2020-10-15T06:38:00.000+00:00", "provider": "email", "providerUid": "user@example.com", @@ -2008,6 +2159,7 @@ def test_get_session(self, m): "$createdAt": "2020-10-15T06:38:00.000+00:00", "$updatedAt": "2020-10-15T06:38:00.000+00:00", "userId": "5e5bb8c16897e", + "userName": "John Doe", "expire": "2020-10-15T06:38:00.000+00:00", "provider": "email", "providerUid": "user@example.com", @@ -2061,6 +2213,7 @@ def test_update_session(self, m): "$createdAt": "2020-10-15T06:38:00.000+00:00", "$updatedAt": "2020-10-15T06:38:00.000+00:00", "userId": "5e5bb8c16897e", + "userName": "John Doe", "expire": "2020-10-15T06:38:00.000+00:00", "provider": "email", "providerUid": "user@example.com", diff --git a/test/services/test_activities.py b/test/services/test_activities.py index f6fe64d5..69646c9b 100644 --- a/test/services/test_activities.py +++ b/test/services/test_activities.py @@ -80,6 +80,7 @@ def test_get_event(self, m): "hostname": "appwrite.io", "sdk": "web", "sdkVersion": "14.0.0", + "reason": "Reported as spam by 25 users in the last 24 hours", } headers = {'Content-Type': 'application/json'} m.request( diff --git a/test/services/test_organization.py b/test/services/test_organization.py index 6f7eee50..1a0ff912 100644 --- a/test/services/test_organization.py +++ b/test/services/test_organization.py @@ -26,6 +26,8 @@ def test_get(self, m): "total": 7.0, "prefs": {}, "budgetAlerts": [], + "budgetNotify": [], + "budgetCritical": [], "billingPlan": "tier-1", "billingPlanId": "tier-1", "billingPlanDetails": { @@ -133,6 +135,8 @@ def test_update(self, m): "total": 7.0, "prefs": {}, "budgetAlerts": [], + "budgetNotify": [], + "budgetCritical": [], "billingPlan": "tier-1", "billingPlanId": "tier-1", "billingPlanDetails": { diff --git a/test/services/test_project.py b/test/services/test_project.py index 35d60355..30ed0b91 100644 --- a/test/services/test_project.py +++ b/test/services/test_project.py @@ -145,7 +145,7 @@ def test_update_auth_method(self, m): def test_list_environments(self, m): data = { "total": 5.0, - "projects": [], + "environments": [], } headers = {'Content-Type': 'application/json'} m.request( @@ -176,32 +176,11 @@ def test_list_environments(self, m): @requests_mock.Mocker() def test_create_environment(self, m): data = { - "$id": "5e5ea5c16897e", + "$id": "staging", "$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", - "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", + "projectId": "5e5ea5c16897e", + "name": "Staging", } headers = {'Content-Type': 'application/json'} m.request( @@ -235,32 +214,11 @@ def test_create_environment(self, m): @requests_mock.Mocker() def test_get_environment(self, m): data = { - "$id": "5e5ea5c16897e", + "$id": "staging", "$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", - "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", + "projectId": "5e5ea5c16897e", + "name": "Staging", } headers = {'Content-Type': 'application/json'} m.request( @@ -285,32 +243,11 @@ def test_get_environment(self, m): @requests_mock.Mocker() def test_update_environment(self, m): data = { - "$id": "5e5ea5c16897e", + "$id": "staging", "$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", - "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", + "projectId": "5e5ea5c16897e", + "name": "Staging", } headers = {'Content-Type': 'application/json'} m.request( @@ -2501,6 +2438,80 @@ def test_update_o_auth2_twitch(self, m): ) self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() + def test_update_o_auth2_vipps(self, m): + data = { + "$id": "github", + "enabled": True, + "clientId": "9a3a4b3e-0000-0000-0000-0f1a6d1c3b2e", + "clientSecret": "Ay1jb000000000000000000000000000000Vx3k", + } + headers = {'Content-Type': 'application/json'} + m.request( + 'PATCH', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.project.update_o_auth2_vipps( + client_id='', + client_secret='', + enabled=False, + ) + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/project/oauth2/vipps', + ) + sent = json.loads(request.body or '{}') + self.assertEqual( + sent, + { + 'clientId': '', + 'clientSecret': '', + 'enabled': False, + }, + ) + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_update_o_auth2_vipps_sandbox(self, m): + data = { + "$id": "github", + "enabled": True, + "clientId": "9a3a4b3e-0000-0000-0000-0f1a6d1c3b2e", + "clientSecret": "Ay1jb000000000000000000000000000000Vx3k", + } + headers = {'Content-Type': 'application/json'} + m.request( + 'PATCH', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.project.update_o_auth2_vipps_sandbox( + client_id='', + client_secret='', + enabled=False, + ) + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/project/oauth2/vippsSandbox', + ) + sent = json.loads(request.body or '{}') + self.assertEqual( + sent, + { + 'clientId': '', + 'clientSecret': '', + 'enabled': False, + }, + ) + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_update_o_auth2_webflow(self, m): data = { @@ -3300,6 +3311,61 @@ def test_list_policies(self, m): ) self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() + def test_update_account_devices_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", + "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( + 'PATCH', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.project.update_account_devices_policy( + False, + ) + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/project/policies/account-devices', + ) + sent = json.loads(request.body or '{}') + self.assertEqual( + sent, + { + 'enabled': False, + }, + ) + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_update_deny_aliased_email_policy(self, m): data = { @@ -4386,6 +4452,55 @@ def test_get_policy(self, m): self.assertEqual(sent, {}) self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() + def test_get_prefs(self, m): + data = {} + headers = {'Content-Type': 'application/json'} + m.request( + 'GET', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.project.get_prefs() + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/project/prefs', + ) + sent = parse_qs(url.query, keep_blank_values=True) + self.assertEqual(sent, {}) + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_update_prefs(self, m): + data = {} + headers = {'Content-Type': 'application/json'} + m.request( + 'PUT', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.project.update_prefs( + {}, + ) + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/project/prefs', + ) + sent = json.loads(request.body or '{}') + self.assertEqual( + sent, + { + 'prefs': {}, + }, + ) + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_update_protocol(self, m): data = { diff --git a/test/services/test_proxy.py b/test/services/test_proxy.py index f20f3d13..b9c80598 100644 --- a/test/services/test_proxy.py +++ b/test/services/test_proxy.py @@ -102,6 +102,8 @@ def test_create_api_rule(self, m): "status": "verified", "logs": "Verification of DNS records failed with DNS resolver 8.8.8.8. Domain stage.myapp.com does not have DNS record.", "renewAt": "datetime", + "expireAt": "2020-10-15T06:38:00.000+00:00", + "certificateStatus": "issued", "protocol": "http", } headers = {'Content-Type': 'application/json'} @@ -146,6 +148,8 @@ def test_create_function_rule(self, m): "status": "verified", "logs": "Verification of DNS records failed with DNS resolver 8.8.8.8. Domain stage.myapp.com does not have DNS record.", "renewAt": "datetime", + "expireAt": "2020-10-15T06:38:00.000+00:00", + "certificateStatus": "issued", "protocol": "http", } headers = {'Content-Type': 'application/json'} @@ -194,6 +198,8 @@ def test_create_redirect_rule(self, m): "status": "verified", "logs": "Verification of DNS records failed with DNS resolver 8.8.8.8. Domain stage.myapp.com does not have DNS record.", "renewAt": "datetime", + "expireAt": "2020-10-15T06:38:00.000+00:00", + "certificateStatus": "issued", "protocol": "http", } headers = {'Content-Type': 'application/json'} @@ -246,6 +252,8 @@ def test_create_site_rule(self, m): "status": "verified", "logs": "Verification of DNS records failed with DNS resolver 8.8.8.8. Domain stage.myapp.com does not have DNS record.", "renewAt": "datetime", + "expireAt": "2020-10-15T06:38:00.000+00:00", + "certificateStatus": "issued", "protocol": "http", } headers = {'Content-Type': 'application/json'} @@ -294,6 +302,8 @@ def test_create_smtp_rule(self, m): "status": "verified", "logs": "Verification of DNS records failed with DNS resolver 8.8.8.8. Domain stage.myapp.com does not have DNS record.", "renewAt": "datetime", + "expireAt": "2020-10-15T06:38:00.000+00:00", + "certificateStatus": "issued", "protocol": "http", } headers = {'Content-Type': 'application/json'} @@ -342,6 +352,8 @@ def test_get_rule(self, m): "status": "verified", "logs": "Verification of DNS records failed with DNS resolver 8.8.8.8. Domain stage.myapp.com does not have DNS record.", "renewAt": "datetime", + "expireAt": "2020-10-15T06:38:00.000+00:00", + "certificateStatus": "issued", "protocol": "http", } headers = {'Content-Type': 'application/json'} @@ -387,6 +399,157 @@ def test_delete_rule(self, m): self.assertEqual(sent, {}) self.assertEqual(response, data) + @requests_mock.Mocker() + def test_update_function_rule(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "domain": "appwrite.company.com", + "type": "deployment", + "trigger": "manual", + "redirectUrl": "https:\/\/appwrite.io\/docs", + "redirectStatusCode": 301.0, + "deploymentId": "n3u9feiwmf", + "deploymentResourceId": "n3u9feiwmf", + "deploymentVcsProviderBranch": "main", + "status": "verified", + "logs": "Verification of DNS records failed with DNS resolver 8.8.8.8. Domain stage.myapp.com does not have DNS record.", + "renewAt": "datetime", + "expireAt": "2020-10-15T06:38:00.000+00:00", + "certificateStatus": "issued", + "protocol": "http", + } + headers = {'Content-Type': 'application/json'} + m.request( + 'PATCH', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.proxy.update_function_rule( + '', + '', + branch='', + ) + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/proxy/rules//function', + ) + sent = json.loads(request.body or '{}') + self.assertEqual( + sent, + { + 'functionId': '', + 'branch': '', + }, + ) + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_update_redirect_rule(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "domain": "appwrite.company.com", + "type": "deployment", + "trigger": "manual", + "redirectUrl": "https:\/\/appwrite.io\/docs", + "redirectStatusCode": 301.0, + "deploymentId": "n3u9feiwmf", + "deploymentResourceId": "n3u9feiwmf", + "deploymentVcsProviderBranch": "main", + "status": "verified", + "logs": "Verification of DNS records failed with DNS resolver 8.8.8.8. Domain stage.myapp.com does not have DNS record.", + "renewAt": "datetime", + "expireAt": "2020-10-15T06:38:00.000+00:00", + "certificateStatus": "issued", + "protocol": "http", + } + headers = {'Content-Type': 'application/json'} + m.request( + 'PATCH', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.proxy.update_redirect_rule( + '', + 'https://example.com', + '301', + '', + 'site', + ) + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/proxy/rules//redirect', + ) + sent = json.loads(request.body or '{}') + self.assertEqual( + sent, + { + 'url': 'https://example.com', + 'statusCode': '301', + 'resourceId': '', + 'resourceType': 'site', + }, + ) + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_update_site_rule(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "domain": "appwrite.company.com", + "type": "deployment", + "trigger": "manual", + "redirectUrl": "https:\/\/appwrite.io\/docs", + "redirectStatusCode": 301.0, + "deploymentId": "n3u9feiwmf", + "deploymentResourceId": "n3u9feiwmf", + "deploymentVcsProviderBranch": "main", + "status": "verified", + "logs": "Verification of DNS records failed with DNS resolver 8.8.8.8. Domain stage.myapp.com does not have DNS record.", + "renewAt": "datetime", + "expireAt": "2020-10-15T06:38:00.000+00:00", + "certificateStatus": "issued", + "protocol": "http", + } + headers = {'Content-Type': 'application/json'} + m.request( + 'PATCH', + requests_mock.ANY, + text=json.dumps(data), + headers=headers, + ) + response = self.proxy.update_site_rule( + '', + '', + branch='', + ) + request = m.last_request + url = urlsplit(request.url) + self.assertEqual( + unquote(url.path), + '/v1/proxy/rules//site', + ) + sent = json.loads(request.body or '{}') + self.assertEqual( + sent, + { + 'siteId': '', + 'branch': '', + }, + ) + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_update_rule_status(self, m): data = { @@ -404,6 +567,8 @@ def test_update_rule_status(self, m): "status": "verified", "logs": "Verification of DNS records failed with DNS resolver 8.8.8.8. Domain stage.myapp.com does not have DNS record.", "renewAt": "datetime", + "expireAt": "2020-10-15T06:38:00.000+00:00", + "certificateStatus": "issued", "protocol": "http", } headers = {'Content-Type': 'application/json'} diff --git a/test/services/test_users.py b/test/services/test_users.py index 5d762871..f7e61657 100644 --- a/test/services/test_users.py +++ b/test/services/test_users.py @@ -1534,6 +1534,7 @@ def test_create_session(self, m): "$createdAt": "2020-10-15T06:38:00.000+00:00", "$updatedAt": "2020-10-15T06:38:00.000+00:00", "userId": "5e5bb8c16897e", + "userName": "John Doe", "expire": "2020-10-15T06:38:00.000+00:00", "provider": "email", "providerUid": "user@example.com", diff --git a/test/services/test_waf.py b/test/services/test_waf.py index db44f9f5..1c1431a8 100644 --- a/test/services/test_waf.py +++ b/test/services/test_waf.py @@ -84,6 +84,7 @@ def test_create_bypass_rule(self, m): priority=1, enabled=False, conditions=[], + schedule={}, ) request = m.last_request url = urlsplit(request.url) @@ -103,6 +104,7 @@ def test_create_bypass_rule(self, m): 'priority': 1, 'enabled': False, 'conditions': [], + 'schedule': {}, }, ) self.assertEqual(response.to_dict(), data) @@ -141,6 +143,7 @@ def test_update_bypass_rule(self, m): priority=1, enabled=False, conditions=[], + schedule={}, ) request = m.last_request url = urlsplit(request.url) @@ -159,6 +162,7 @@ def test_update_bypass_rule(self, m): 'priority': 1, 'enabled': False, 'conditions': [], + 'schedule': {}, }, ) self.assertEqual(response.to_dict(), data) @@ -203,6 +207,7 @@ def test_create_challenge_rule(self, m): conditions=[], difficulty=1, ttl=1, + schedule={}, ) request = m.last_request url = urlsplit(request.url) @@ -225,6 +230,7 @@ def test_create_challenge_rule(self, m): 'conditions': [], 'difficulty': 1, 'ttl': 1, + 'schedule': {}, }, ) self.assertEqual(response.to_dict(), data) @@ -269,6 +275,7 @@ def test_update_challenge_rule(self, m): conditions=[], difficulty=1, ttl=1, + schedule={}, ) request = m.last_request url = urlsplit(request.url) @@ -290,6 +297,7 @@ def test_update_challenge_rule(self, m): 'conditions': [], 'difficulty': 1, 'ttl': 1, + 'schedule': {}, }, ) self.assertEqual(response.to_dict(), data) @@ -328,6 +336,7 @@ def test_create_deny_rule(self, m): priority=1, enabled=False, conditions=[], + schedule={}, ) request = m.last_request url = urlsplit(request.url) @@ -347,6 +356,7 @@ def test_create_deny_rule(self, m): 'priority': 1, 'enabled': False, 'conditions': [], + 'schedule': {}, }, ) self.assertEqual(response.to_dict(), data) @@ -385,6 +395,7 @@ def test_update_deny_rule(self, m): priority=1, enabled=False, conditions=[], + schedule={}, ) request = m.last_request url = urlsplit(request.url) @@ -403,6 +414,7 @@ def test_update_deny_rule(self, m): 'priority': 1, 'enabled': False, 'conditions': [], + 'schedule': {}, }, ) self.assertEqual(response.to_dict(), data) @@ -451,6 +463,7 @@ def test_create_rate_limit_rule(self, m): priority=1, enabled=False, conditions=[], + schedule={}, ) request = m.last_request url = urlsplit(request.url) @@ -475,6 +488,7 @@ def test_create_rate_limit_rule(self, m): 'priority': 1, 'enabled': False, 'conditions': [], + 'schedule': {}, }, ) self.assertEqual(response.to_dict(), data) @@ -522,6 +536,7 @@ def test_update_rate_limit_rule(self, m): priority=1, enabled=False, conditions=[], + schedule={}, ) request = m.last_request url = urlsplit(request.url) @@ -544,6 +559,7 @@ def test_update_rate_limit_rule(self, m): 'priority': 1, 'enabled': False, 'conditions': [], + 'schedule': {}, }, ) self.assertEqual(response.to_dict(), data) @@ -586,6 +602,7 @@ def test_create_redirect_rule(self, m): priority=1, enabled=False, conditions=[], + schedule={}, ) request = m.last_request url = urlsplit(request.url) @@ -607,6 +624,7 @@ def test_create_redirect_rule(self, m): 'priority': 1, 'enabled': False, 'conditions': [], + 'schedule': {}, }, ) self.assertEqual(response.to_dict(), data) @@ -649,6 +667,7 @@ def test_update_redirect_rule(self, m): priority=1, enabled=False, conditions=[], + schedule={}, ) request = m.last_request url = urlsplit(request.url) @@ -669,6 +688,7 @@ def test_update_redirect_rule(self, m): 'priority': 1, 'enabled': False, 'conditions': [], + 'schedule': {}, }, ) self.assertEqual(response.to_dict(), data)