diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 53679c3b..dfbf4972 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,14 +2,28 @@ name: Publish to PyPI on: release: types: [published] + workflow_dispatch: + inputs: + tag: + description: 'Release tag to publish, e.g. 1.2.3' + required: true jobs: publish: name: Release build and publish runs-on: ubuntu-latest + permissions: + # Mints the short-lived OIDC token PyPI exchanges for an upload token, so + # no long-lived API token has to be stored on the repository. + id-token: write steps: - name: Check out code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # A release build must come from the tag, never from a branch that may + # have moved on. On a release event github.ref is already the tag; on a + # manual retry the tag has to be named explicitly. + ref: ${{ inputs.tag || github.ref }} - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -22,10 +36,4 @@ jobs: python setup.py sdist bdist_wheel - name: Publish package - run: | - python -m pip install twine - python -m twine upload -r pypi dist/* - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - TWINE_NON_INTERACTIVE: true + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eba1752..52fb6a98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change Log +## 23.0.0 + +* Breaking: removed `account.createJWT`; use `users.createJWT` instead. A leaked JWT could mint further JWTs, letting a credential outlive its own expiry — a session cannot duplicate itself to live forever either +* Breaking: removed `project.createKey`. A leaked key could mint further hidden keys, making a compromise far harder to contain and revoke +* Breaking: `activities.listEvents` `queries` now takes an array instead of a string +* Added: `embeddings` service with `createTextEmbeddings`, plus the `EmbeddingModel` enum +* Added: TablesDB migration methods `listMigrations`, `createMigration`, `getMigration`, `deleteMigration`, `cutoverMigration`, and `listOperations` +* Added: `proxy.createInvalidation` for purging cached edge responses, plus the `InvalidationType` enum +* Added: `apps.deleteInstallation` +* Added: `users.getMFAChallenge` and the `MfaChallengeSecret` model +* Added: `project.updateMFAFactorsPolicy` and the `PolicyMfaFactors` model +* Added: `client.setOrganization` for organization-scoped requests +* Added: `folder` parameter to `storage.createFile` +* Added: `syncMode` parameter to `tablesDB.create` and `tablesDB.update`, and `specification` to `tablesDB.update` +* Added: `installationScopes` parameter to `project.updateOAuth2Server` +* Added: `custom` authentication factor, `node-26` runtime, `mfa-factors` project policy, and the `embeddings.write` and `proxy.invalidations.write` key scopes +* Updated: response format to `1.9.6` + ## 22.2.0 * Added: `apps` installation and key management methods (`list_installations`, `get_installation`, `create_installation_token`, `list_installation_scopes`, `list_keys`, `create_key`, `get_key`, `delete_key`) diff --git a/README.md b/README.md index 60a89373..926d11a4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Appwrite Python SDK ![License](https://img.shields.io/github/license/appwrite/sdk-for-python.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.9.5-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.9.6-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) diff --git a/appwrite/client.py b/appwrite/client.py index 6e1d40e7..67ba5631 100644 --- a/appwrite/client.py +++ b/appwrite/client.py @@ -17,12 +17,12 @@ def __init__(self): self._endpoint = 'https://cloud.appwrite.io/v1' self._global_headers = { 'content-type': '', - 'user-agent' : f'AppwritePythonSDK/22.2.0 ({platform.uname().system}; {platform.uname().version}; {platform.uname().machine})', + 'user-agent' : f'AppwritePythonSDK/23.0.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': '22.2.0', - 'X-Appwrite-Response-Format' : '1.9.5', + 'x-sdk-version': '23.0.0', + 'X-Appwrite-Response-Format' : '1.9.6', } self._config = {} @@ -60,6 +60,13 @@ def set_key(self, value): self._config['key'] = value return self + def set_organization(self, value): + """Your organization ID""" + + self._global_headers['x-appwrite-organization'] = value + self._config['organization'] = value + return self + def set_jwt(self, value): """Your secret JSON Web Token""" diff --git a/appwrite/encoders/value_class_encoder.py b/appwrite/encoders/value_class_encoder.py index b725fb9b..ab46a105 100644 --- a/appwrite/encoders/value_class_encoder.py +++ b/appwrite/encoders/value_class_encoder.py @@ -15,6 +15,7 @@ from ..enums.relation_mutate import RelationMutate from ..enums.databases_index_type import DatabasesIndexType from ..enums.order_by import OrderBy +from ..enums.embedding_model import EmbeddingModel from ..enums.runtime import Runtime from ..enums.project_key_scopes import ProjectKeyScopes from ..enums.template_reference_type import TemplateReferenceType @@ -35,6 +36,7 @@ from ..enums.project_smtp_secure import ProjectSMTPSecure from ..enums.project_email_template_id import ProjectEmailTemplateId from ..enums.project_email_template_locale import ProjectEmailTemplateLocale +from ..enums.invalidation_type import InvalidationType from ..enums.status_code import StatusCode from ..enums.proxy_resource_type import ProxyResourceType from ..enums.framework import Framework @@ -111,6 +113,9 @@ def default(self, o): if isinstance(o, OrderBy): return o.value + if isinstance(o, EmbeddingModel): + return o.value + if isinstance(o, Runtime): return o.value @@ -171,6 +176,9 @@ def default(self, o): if isinstance(o, ProjectEmailTemplateLocale): return o.value + if isinstance(o, InvalidationType): + return o.value + if isinstance(o, StatusCode): return o.value diff --git a/appwrite/enums/authentication_factor.py b/appwrite/enums/authentication_factor.py index a5b8cf2a..9e1f1add 100644 --- a/appwrite/enums/authentication_factor.py +++ b/appwrite/enums/authentication_factor.py @@ -5,3 +5,4 @@ class AuthenticationFactor(Enum): PHONE = "phone" TOTP = "totp" RECOVERYCODE = "recoverycode" + CUSTOM = "custom" diff --git a/appwrite/enums/build_runtime.py b/appwrite/enums/build_runtime.py index 7b1c6dc1..8472bd82 100644 --- a/appwrite/enums/build_runtime.py +++ b/appwrite/enums/build_runtime.py @@ -11,6 +11,7 @@ class BuildRuntime(Enum): NODE_23 = "node-23" NODE_24 = "node-24" NODE_25 = "node-25" + NODE_26 = "node-26" PHP_8_0 = "php-8.0" PHP_8_1 = "php-8.1" PHP_8_2 = "php-8.2" diff --git a/appwrite/enums/embedding_model.py b/appwrite/enums/embedding_model.py new file mode 100644 index 00000000..5bbf1cf9 --- /dev/null +++ b/appwrite/enums/embedding_model.py @@ -0,0 +1,7 @@ +from enum import Enum + +class EmbeddingModel(Enum): + NOMIC_EMBED_TEXT = "nomic-embed-text" + EMBEDDING_GEMMA = "embedding-gemma" + ALL_MINILM = "all-minilm" + BGE_SMALL = "bge-small" diff --git a/appwrite/enums/invalidation_type.py b/appwrite/enums/invalidation_type.py new file mode 100644 index 00000000..1a8f7707 --- /dev/null +++ b/appwrite/enums/invalidation_type.py @@ -0,0 +1,6 @@ +from enum import Enum + +class InvalidationType(Enum): + TAG = "tag" + PATH = "path" + ALL = "all" diff --git a/appwrite/enums/project_key_scopes.py b/appwrite/enums/project_key_scopes.py index e29a2454..d96ff939 100644 --- a/appwrite/enums/project_key_scopes.py +++ b/appwrite/enums/project_key_scopes.py @@ -35,6 +35,7 @@ class ProjectKeyScopes(Enum): INDEXES_WRITE = "indexes.write" ROWS_READ = "rows.read" ROWS_WRITE = "rows.write" + EMBEDDINGS_WRITE = "embeddings.write" COLLECTIONS_READ = "collections.read" COLLECTIONS_WRITE = "collections.write" ATTRIBUTES_READ = "attributes.read" @@ -99,6 +100,7 @@ class ProjectKeyScopes(Enum): WAFRULES_READ = "wafRules.read" WAFRULES_WRITE = "wafRules.write" EVENTS_READ = "events.read" + PROXY_INVALIDATIONS_WRITE = "proxy.invalidations.write" APPS_READ = "apps.read" APPS_WRITE = "apps.write" OAUTH2_READ = "oauth2.read" diff --git a/appwrite/enums/project_policy_id.py b/appwrite/enums/project_policy_id.py index 56ca022b..02b97a3e 100644 --- a/appwrite/enums/project_policy_id.py +++ b/appwrite/enums/project_policy_id.py @@ -11,6 +11,7 @@ class ProjectPolicyId(Enum): SESSION_LIMIT = "session-limit" USER_LIMIT = "user-limit" MEMBERSHIP_PRIVACY = "membership-privacy" + MFA_FACTORS = "mfa-factors" DENY_ALIASED_EMAIL = "deny-aliased-email" DENY_DISPOSABLE_EMAIL = "deny-disposable-email" DENY_FREE_EMAIL = "deny-free-email" diff --git a/appwrite/enums/runtime.py b/appwrite/enums/runtime.py index 74e3a279..0bc261ca 100644 --- a/appwrite/enums/runtime.py +++ b/appwrite/enums/runtime.py @@ -11,6 +11,7 @@ class Runtime(Enum): NODE_23 = "node-23" NODE_24 = "node-24" NODE_25 = "node-25" + NODE_26 = "node-26" PHP_8_0 = "php-8.0" PHP_8_1 = "php-8.1" PHP_8_2 = "php-8.2" diff --git a/appwrite/models/__init__.py b/appwrite/models/__init__.py index 419db211..9b7d0ced 100644 --- a/appwrite/models/__init__.py +++ b/appwrite/models/__init__.py @@ -43,9 +43,11 @@ from .target_list import TargetList from .transaction_list import TransactionList from .specification_list import SpecificationList +from .embedding_list import EmbeddingList from .insight_list import InsightList from .report_list import ReportList from .database import Database +from .embedding import Embedding from .collection import Collection from .attribute_list import AttributeList from .attribute_string import AttributeString @@ -180,6 +182,7 @@ from .policy_session_limit import PolicySessionLimit from .policy_user_limit import PolicyUserLimit from .policy_membership_privacy import PolicyMembershipPrivacy +from .policy_mfa_factors import PolicyMfaFactors from .platform_web import PlatformWeb from .platform_apple import PlatformApple from .platform_android import PlatformAndroid @@ -197,6 +200,7 @@ from .proxy_rule import ProxyRule from .email_template import EmailTemplate from .mfa_challenge import MfaChallenge +from .mfa_challenge_secret import MfaChallengeSecret from .mfa_recovery_codes import MfaRecoveryCodes from .mfa_type import MfaType from .mfa_factors import MfaFactors @@ -220,10 +224,14 @@ from .billing_plan_dedicated_database_limits import BillingPlanDedicatedDatabaseLimits from .billing_plan_supported_addons import BillingPlanSupportedAddons from .block import Block +from .database_migration import DatabaseMigration from .dedicated_database import DedicatedDatabase from .database_status import DatabaseStatus from .dedicated_database_member import DedicatedDatabaseMember +from .dedicated_database_operation import DedicatedDatabaseOperation +from .dedicated_database_operation_list import DedicatedDatabaseOperationList from .dedicated_database_replicas import DedicatedDatabaseReplicas +from .proxy_invalidation import ProxyInvalidation from .organization import Organization from .backup_policy import BackupPolicy from .policy_deny_aliased_email import PolicyDenyAliasedEmail @@ -264,6 +272,7 @@ from .backup_archive_list import BackupArchiveList from .backup_policy_list import BackupPolicyList from .backup_restoration_list import BackupRestorationList +from .database_migration_list import DatabaseMigrationList from .apps_list import AppsList from .app_secret_list import AppSecretList from .app_scope_list import AppScopeList @@ -316,9 +325,11 @@ 'TargetList', 'TransactionList', 'SpecificationList', + 'EmbeddingList', 'InsightList', 'ReportList', 'Database', + 'Embedding', 'Collection', 'AttributeList', 'AttributeString', @@ -453,6 +464,7 @@ 'PolicySessionLimit', 'PolicyUserLimit', 'PolicyMembershipPrivacy', + 'PolicyMfaFactors', 'PlatformWeb', 'PlatformApple', 'PlatformAndroid', @@ -470,6 +482,7 @@ 'ProxyRule', 'EmailTemplate', 'MfaChallenge', + 'MfaChallengeSecret', 'MfaRecoveryCodes', 'MfaType', 'MfaFactors', @@ -493,10 +506,14 @@ 'BillingPlanDedicatedDatabaseLimits', 'BillingPlanSupportedAddons', 'Block', + 'DatabaseMigration', 'DedicatedDatabase', 'DatabaseStatus', 'DedicatedDatabaseMember', + 'DedicatedDatabaseOperation', + 'DedicatedDatabaseOperationList', 'DedicatedDatabaseReplicas', + 'ProxyInvalidation', 'Organization', 'BackupPolicy', 'PolicyDenyAliasedEmail', @@ -537,6 +554,7 @@ 'BackupArchiveList', 'BackupPolicyList', 'BackupRestorationList', + 'DatabaseMigrationList', 'AppsList', 'AppSecretList', 'AppScopeList', diff --git a/appwrite/models/billing_plan.py b/appwrite/models/billing_plan.py index c37db9d3..dfe4d167 100644 --- a/appwrite/models/billing_plan.py +++ b/appwrite/models/billing_plan.py @@ -36,7 +36,7 @@ class BillingPlan(AppwriteModel): Image Transformations screenshotsgenerated : float Screenshots generated - members : float + members : Optional[float] Members webhooks : float Webhooks @@ -84,7 +84,7 @@ class BillingPlan(AppwriteModel): SMS authentications per month domains : float Custom domains - activitylogs : float + activitylogs : Optional[float] Activity log days usagelogs : float Usage history days @@ -132,13 +132,13 @@ class BillingPlan(AppwriteModel): Does plan support restricting sign-ups to corporate email addresses only. supportsprojectspecificroles : bool Does plan support project-specific member roles. - backupsenabled : bool + backupsenabled : Optional[bool] Does plan support backup policies. usageperproject : bool Whether usage addons are calculated per project. supportedaddons : BillingPlanSupportedAddons Supported addons for this plan - backuppolicies : float + backuppolicies : Optional[float] How many policies does plan support deploymentsize : float Maximum function and site deployment size in MB @@ -165,7 +165,7 @@ class BillingPlan(AppwriteModel): storage: float = Field(..., alias='storage') imagetransformations: float = Field(..., alias='imageTransformations') screenshotsgenerated: float = Field(..., alias='screenshotsGenerated') - members: float = Field(..., alias='members') + members: Optional[float] = Field(default=None, alias='members') webhooks: float = Field(..., alias='webhooks') wafrules: float = Field(..., alias='wafRules') projects: float = Field(..., alias='projects') @@ -189,7 +189,7 @@ class BillingPlan(AppwriteModel): topics: float = Field(..., alias='topics') authphone: float = Field(..., alias='authPhone') domains: float = Field(..., alias='domains') - activitylogs: float = Field(..., alias='activityLogs') + activitylogs: Optional[float] = Field(default=None, alias='activityLogs') usagelogs: float = Field(..., alias='usageLogs') usagelogsintervals: Optional[List[Any]] = Field(default=None, alias='usageLogsIntervals') projectinactivitydays: float = Field(..., alias='projectInactivityDays') @@ -213,10 +213,10 @@ class BillingPlan(AppwriteModel): supportsfreeemailvalidation: bool = Field(..., alias='supportsFreeEmailValidation') supportscorporateemailvalidation: bool = Field(..., alias='supportsCorporateEmailValidation') supportsprojectspecificroles: bool = Field(..., alias='supportsProjectSpecificRoles') - backupsenabled: bool = Field(..., alias='backupsEnabled') + backupsenabled: Optional[bool] = Field(default=None, alias='backupsEnabled') usageperproject: bool = Field(..., alias='usagePerProject') supportedaddons: BillingPlanSupportedAddons = Field(..., alias='supportedAddons') - backuppolicies: float = Field(..., alias='backupPolicies') + backuppolicies: Optional[float] = Field(default=None, alias='backupPolicies') deploymentsize: float = Field(..., alias='deploymentSize') buildsize: float = Field(..., alias='buildSize') databasesallowencrypt: bool = Field(..., alias='databasesAllowEncrypt') diff --git a/appwrite/models/billing_plan_addon.py b/appwrite/models/billing_plan_addon.py index b1d75b34..6ad96a36 100644 --- a/appwrite/models/billing_plan_addon.py +++ b/appwrite/models/billing_plan_addon.py @@ -10,10 +10,10 @@ class BillingPlanAddon(AppwriteModel): Attributes ---------- - seats : BillingPlanAddonDetails + seats : Optional[BillingPlanAddonDetails] Addon seats - projects : BillingPlanAddonDetails + projects : Optional[BillingPlanAddonDetails] Addon projects """ - seats: BillingPlanAddonDetails = Field(..., alias='seats') - projects: BillingPlanAddonDetails = Field(..., alias='projects') + seats: Optional[BillingPlanAddonDetails] = Field(default=None, alias='seats') + projects: Optional[BillingPlanAddonDetails] = Field(default=None, alias='projects') diff --git a/appwrite/models/billing_plan_addon_details.py b/appwrite/models/billing_plan_addon_details.py index 8acd360c..67b2754a 100644 --- a/appwrite/models/billing_plan_addon_details.py +++ b/appwrite/models/billing_plan_addon_details.py @@ -17,7 +17,7 @@ class BillingPlanAddonDetails(AppwriteModel): Addon limit type : str Addon type - currency : str + currency : Optional[str] Price currency price : float Price @@ -30,7 +30,7 @@ class BillingPlanAddonDetails(AppwriteModel): planincluded: float = Field(..., alias='planIncluded') limit: float = Field(..., alias='limit') type: str = Field(..., alias='type') - currency: str = Field(..., alias='currency') + currency: Optional[str] = Field(default=None, alias='currency') price: float = Field(..., alias='price') value: float = Field(..., alias='value') invoicedesc: str = Field(..., alias='invoiceDesc') diff --git a/appwrite/models/database.py b/appwrite/models/database.py index c1852901..6c02040a 100644 --- a/appwrite/models/database.py +++ b/appwrite/models/database.py @@ -28,7 +28,7 @@ class Database(AppwriteModel): status : Optional[DatabaseStatus] Dedicated database lifecycle status. Null when the database has no valid dedicated backing. engine : Optional[str] - Underlying engine of the dedicated backing: postgresql, mysql, mariadb, or mongodb. A managed product (tablesdb, documentsdb, vectorsdb) reports the engine it runs on, so its type and engine can differ. Null when the database has no dedicated backing. + Underlying engine of the dedicated backing: postgresql, mysql, or mongodb. A managed product (tablesdb, documentsdb, vectorsdb) reports the engine it runs on, so its type and engine can differ. Null when the database has no dedicated backing. specification : Optional[str] Compute specification identifier of the dedicated backing, e.g. s-2vcpu-2gb. Null when the database has no dedicated backing. replicas : Optional[float] diff --git a/appwrite/models/database_migration.py b/appwrite/models/database_migration.py new file mode 100644 index 00000000..f1bcea3b --- /dev/null +++ b/appwrite/models/database_migration.py @@ -0,0 +1,60 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel + +class DatabaseMigration(AppwriteModel): + """ + Database Migration + + Attributes + ---------- + id : str + Database migration ID. + createdat : str + Migration creation time in ISO 8601 format. + updatedat : str + Migration update time in ISO 8601 format. + projectid : str + Project ID that owns the migrating database. + databaseid : str + Logical database ID being migrated. + specification : str + Dedicated compute specification provisioned for the migration target. + phase : str + Migration phase. Possible values: pending, provisioned, capturing, backfilling, catching_up, verifying, ready_to_cutover, cutover, soaking, done, failed, rolled_back. + attempt : float + Number of times a migration step has failed and been recorded. + lasterror : str + Reason the most recent migration step failed, empty while none has. + lagdocuments : float + Number of documents still pending replication to the target. + verifiedat : str + Time the migrated data was verified against the source in ISO 8601 format. + cutoverat : str + Time routing was flipped to the target in ISO 8601 format. + soakuntil : str + Time the post-cutover soak window ends in ISO 8601 format. + autocutover : bool + Whether the migration cuts over automatically once ready. Set when the migration is created and never changed afterwards, so it always reports what was asked for. + cutoverrequested : bool + Whether a cutover has been requested and not yet attempted. Set by the cutover endpoint and cleared when the attempt is made, so a cutover that fails a check parks the migration again rather than retrying on its own. + paused : bool + Whether the migration is paused. + """ + id: str = Field(..., alias='$id') + createdat: str = Field(..., alias='$createdAt') + updatedat: str = Field(..., alias='$updatedAt') + projectid: str = Field(..., alias='projectId') + databaseid: str = Field(..., alias='databaseId') + specification: str = Field(..., alias='specification') + phase: str = Field(..., alias='phase') + attempt: float = Field(..., alias='attempt') + lasterror: str = Field(..., alias='lastError') + lagdocuments: float = Field(..., alias='lagDocuments') + verifiedat: str = Field(..., alias='verifiedAt') + cutoverat: str = Field(..., alias='cutoverAt') + soakuntil: str = Field(..., alias='soakUntil') + autocutover: bool = Field(..., alias='autoCutover') + cutoverrequested: bool = Field(..., alias='cutoverRequested') + paused: bool = Field(..., alias='paused') diff --git a/appwrite/models/database_migration_list.py b/appwrite/models/database_migration_list.py new file mode 100644 index 00000000..63571efb --- /dev/null +++ b/appwrite/models/database_migration_list.py @@ -0,0 +1,19 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel +from .database_migration import DatabaseMigration + +class DatabaseMigrationList(AppwriteModel): + """ + Database Migrations List + + Attributes + ---------- + total : float + Total number of migrations that matched your query. + migrations : List[DatabaseMigration] + List of migrations. + """ + total: float = Field(..., alias='total') + migrations: List[DatabaseMigration] = Field(..., alias='migrations') diff --git a/appwrite/models/database_status.py b/appwrite/models/database_status.py index dd469917..34d3c1f6 100644 --- a/appwrite/models/database_status.py +++ b/appwrite/models/database_status.py @@ -13,19 +13,31 @@ class DatabaseStatus(AppwriteModel): Attributes ---------- health : str - Overall health status: healthy, degraded, or unhealthy. + Overall health status: healthy, degraded, unhealthy, or unknown when nothing could be measured. ready : bool Whether the database is ready to accept connections. engine : str - Database engine: postgresql, mysql, mariadb, or mongodb. + Database engine: postgresql, mysql, or mongodb. version : str Database engine version. uptime : float Database uptime in seconds. connections : DatabaseStatusConnections Connection statistics. + syncmode : str + Requested replication sync mode. Possible values: async, sync, quorum. Compare with effectiveSyncMode for what the primary is enforcing. + effectivesyncmode : Optional[str] + Replication sync mode the primary is actually enforcing. Null when high availability is disabled or the state could not be read. + syncdegraded : bool + Whether the enforced replication is weaker than the requested syncMode. + syncacknowledgements : float + Number of standby acknowledgements the primary waits for before a write is committed. + syncstandbycount : float + Number of standbys registered with the primary for synchronous replication. + syncstateconfirmed : Optional[bool] + Whether the other sync fields are an engine reading rather than a recorded estimate. True when the primary answered what it is enforcing, including when that answer contradicted the record, in which case the contradicted values are replaced by the ones the engine reports. False when the reading could not be taken: the probe did not answer, there was no engine to ask, or the values describe a configuration change just applied rather than anything measured. Absent when no engine was asked at all, so an unprobed database is distinguishable from an unconfirmed one. False never means a standby was found lagging, because it is the absence of a reading rather than a negative one, so draw no conclusion about replication health from it or from a response that omits it. replicas : List[DatabaseStatusReplica] - List of database replicas and their status. + List of database replicas and their status. Every configured member appears, including one the backend has not brought up, which is reported as not healthy. volumes : List[DatabaseStatusVolume] Storage volume information. """ @@ -35,5 +47,11 @@ class DatabaseStatus(AppwriteModel): version: str = Field(..., alias='version') uptime: float = Field(..., alias='uptime') connections: DatabaseStatusConnections = Field(..., alias='connections') + syncmode: str = Field(..., alias='syncMode') + effectivesyncmode: Optional[str] = Field(default=None, alias='effectiveSyncMode') + syncdegraded: bool = Field(..., alias='syncDegraded') + syncacknowledgements: float = Field(..., alias='syncAcknowledgements') + syncstandbycount: float = Field(..., alias='syncStandbyCount') + syncstateconfirmed: Optional[bool] = Field(default=None, alias='syncStateConfirmed') replicas: List[DatabaseStatusReplica] = Field(..., alias='replicas') volumes: List[DatabaseStatusVolume] = Field(..., alias='volumes') diff --git a/appwrite/models/database_status_connections.py b/appwrite/models/database_status_connections.py index fe335596..6cfa52e2 100644 --- a/appwrite/models/database_status_connections.py +++ b/appwrite/models/database_status_connections.py @@ -12,7 +12,7 @@ class DatabaseStatusConnections(AppwriteModel): current : float Current number of active connections. max : float - Maximum allowed connections. + The engine's own max_connections. On a pooled database this is the backend limit the pooler multiplexes onto, not the ceiling a client pool may reach — that is networkMaxConnections on the database resource. """ current: float = Field(..., alias='current') max: float = Field(..., alias='max') diff --git a/appwrite/models/database_status_replica.py b/appwrite/models/database_status_replica.py index 73cf79e4..91428e91 100644 --- a/appwrite/models/database_status_replica.py +++ b/appwrite/models/database_status_replica.py @@ -10,9 +10,9 @@ class DatabaseStatusReplica(AppwriteModel): Attributes ---------- index : float - StatefulSet pod index (0 = primary, 1+ = replicas). + Member index within the database. Read `role` for which member accepts writes: a failover moves the primary without renumbering the indexes. role : str - Replica role: primary or replica. + Member role. Possible values: primary (accepts reads and writes), replica (read-only follower), unknown (placement not established; reported while a transition is moving or restarting the topology, so no member can be named the write target). healthy : bool Whether the replica is healthy. lagseconds : Optional[float] diff --git a/appwrite/models/dedicated_database.py b/appwrite/models/dedicated_database.py index 9f943228..810fe7df 100644 --- a/appwrite/models/dedicated_database.py +++ b/appwrite/models/dedicated_database.py @@ -22,7 +22,7 @@ class DedicatedDatabase(AppwriteModel): api : str Product API that owns this database: tablesdb, documentsdb, vectorsdb, mysql, postgresql, or mongodb. engine : str - Database engine: postgresql, mysql, mariadb, or mongodb. + Database engine: postgresql, mysql, or mongodb. Null until the backing reports one. version : str Database engine version. specification : str @@ -32,7 +32,7 @@ class DedicatedDatabase(AppwriteModel): hostname : str Database hostname for connections. connectionport : float - Database port for connections. + Database port for connections. Derived from the engine when the backing has not reported one yet. connectionuser : str Database username for connections. connectionpassword : str @@ -69,10 +69,8 @@ class DedicatedDatabase(AppwriteModel): Number of high availability replicas. High availability is enabled when greater than 0. syncmode : str Replication sync mode: async, sync, or quorum. - crossregionreplicas : float - Number of cross-region replicas. Cross-region availability is enabled when greater than 0. networkmaxconnections : float - Maximum concurrent connections. + Maximum concurrent client connections. This is the limit a client pool may reach; the engine's own max_connections reported by the status endpoint is a smaller backend limit the pooler multiplexes onto and does not constrain a client pool. networkidletimeoutseconds : float Connection idle timeout in seconds. networkipallowlist : List[Any] @@ -138,7 +136,6 @@ class DedicatedDatabase(AppwriteModel): nodepool: str = Field(..., alias='nodePool') replicas: float = Field(..., alias='replicas') syncmode: str = Field(..., alias='syncMode') - crossregionreplicas: float = Field(..., alias='crossRegionReplicas') networkmaxconnections: float = Field(..., alias='networkMaxConnections') networkidletimeoutseconds: float = Field(..., alias='networkIdleTimeoutSeconds') networkipallowlist: List[Any] = Field(..., alias='networkIPAllowlist') diff --git a/appwrite/models/dedicated_database_member.py b/appwrite/models/dedicated_database_member.py index 6cb4daa3..128021bb 100644 --- a/appwrite/models/dedicated_database_member.py +++ b/appwrite/models/dedicated_database_member.py @@ -12,13 +12,13 @@ class DedicatedDatabaseMember(AppwriteModel): id : str Member identifier. role : str - Member role. Possible values: primary (accepts reads and writes), replica (read-only follower). + Member role. Possible values: primary (accepts reads and writes), replica (read-only follower), unknown (placement not established; reported while a transition is moving or restarting the topology and this member has not been probed, so no member can be named the write target). status : str - Member pod status. Possible values: provisioning (pod missing or Pending), starting (Running but not Ready), active (Running and Ready), failed (Failed phase or CrashLoopBackOff container), or the lowercased pod phase reported by the cluster. - lagseconds : float - Replication lag in seconds. + Member pod status. Possible values: pending (configured but absent from the backend topology, so nothing is bringing it up), provisioning (pod missing or Pending), starting (Running but not Ready), active (Running and Ready), failed (Failed phase or CrashLoopBackOff container), or the lowercased pod phase reported by the cluster. + lagseconds : Optional[float] + Replication lag in seconds. Null when the lag is not known: a primary has none to report, and a member the backend has not probed has none yet. """ id: str = Field(..., alias='$id') role: str = Field(..., alias='role') status: str = Field(..., alias='status') - lagseconds: float = Field(..., alias='lagSeconds') + lagseconds: Optional[float] = Field(default=None, alias='lagSeconds') diff --git a/appwrite/models/dedicated_database_operation.py b/appwrite/models/dedicated_database_operation.py new file mode 100644 index 00000000..97d482d3 --- /dev/null +++ b/appwrite/models/dedicated_database_operation.py @@ -0,0 +1,45 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel + +class DedicatedDatabaseOperation(AppwriteModel): + """ + Operation + + Attributes + ---------- + id : str + Operation ID. + createdat : str + Operation creation time in ISO 8601 format. + databaseid : str + Database ID the operation ran against. + type : str + Operation type, such as provision, update, restore, pausing, resuming, failover, backup-create or cross-region-enable. + status : str + Operation status. Possible values: running (in progress), completed (finished successfully), failed (ended in an error). + attempts : float + Number of times this operation has been attempted. + requestedat : Optional[str] + Time the operation was requested, in ISO 8601 format. + startedat : Optional[str] + Time the operation started, in ISO 8601 format. + completedat : Optional[str] + Time the operation reached a terminal state, in ISO 8601 format. + errorcode : str + Machine-readable failure code. `Interrupted` marks an attempt that ended before its outcome could be confirmed. + errormessage : str + Failure message if the operation failed. + """ + id: str = Field(..., alias='$id') + createdat: str = Field(..., alias='$createdAt') + databaseid: str = Field(..., alias='databaseId') + type: str = Field(..., alias='type') + status: str = Field(..., alias='status') + attempts: float = Field(..., alias='attempts') + requestedat: Optional[str] = Field(default=None, alias='requestedAt') + startedat: Optional[str] = Field(default=None, alias='startedAt') + completedat: Optional[str] = Field(default=None, alias='completedAt') + errorcode: str = Field(..., alias='errorCode') + errormessage: str = Field(..., alias='errorMessage') diff --git a/appwrite/models/dedicated_database_operation_list.py b/appwrite/models/dedicated_database_operation_list.py new file mode 100644 index 00000000..012e21d5 --- /dev/null +++ b/appwrite/models/dedicated_database_operation_list.py @@ -0,0 +1,19 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel +from .dedicated_database_operation import DedicatedDatabaseOperation + +class DedicatedDatabaseOperationList(AppwriteModel): + """ + OperationList + + Attributes + ---------- + total : float + Total number of operations. + operations : List[DedicatedDatabaseOperation] + List of operations. + """ + total: float = Field(..., alias='total') + operations: List[DedicatedDatabaseOperation] = Field(..., alias='operations') diff --git a/appwrite/models/dedicated_database_replicas.py b/appwrite/models/dedicated_database_replicas.py index fa19a87a..45e6de40 100644 --- a/appwrite/models/dedicated_database_replicas.py +++ b/appwrite/models/dedicated_database_replicas.py @@ -13,10 +13,25 @@ class DedicatedDatabaseReplicas(AppwriteModel): replicas : float Number of configured replicas. Zero means high availability is disabled. syncmode : str - Replication sync mode. Possible values: async (asynchronous, fastest), sync (synchronous, strong consistency), quorum (quorum-based, majority of replicas must confirm). + Requested replication sync mode. Possible values: async (asynchronous, fastest), sync (synchronous, strong consistency), quorum (quorum-based, majority of replicas must confirm). This is what was asked for; compare it with effectiveSyncMode for what the primary is enforcing. + effectivesyncmode : Optional[str] + Replication sync mode the primary is actually enforcing. Null when high availability is disabled or the state could not be read. A value below the requested syncMode means writes are being acknowledged with weaker durability than configured. + syncdegraded : bool + Whether the enforced replication is weaker than the requested syncMode. + syncacknowledgements : float + Number of standby acknowledgements the primary waits for before a write is committed. Zero means writes are acknowledged locally. + syncstandbycount : float + Number of standbys registered with the primary for synchronous replication. + syncstateconfirmed : Optional[bool] + Whether the other sync fields are an engine reading rather than a recorded estimate. True when the primary answered what it is enforcing, including when that answer contradicted the record, in which case the contradicted values are replaced by the ones the engine reports. False when the reading could not be taken: the probe did not answer, there was no engine to ask, or the values describe a configuration change just applied rather than anything measured. Absent when no engine was asked at all, so an unprobed database is distinguishable from an unconfirmed one. False never means a standby was found lagging, because it is the absence of a reading rather than a negative one, so draw no conclusion about replication health from it or from a response that omits it. members : List[DedicatedDatabaseMember] Per-pod statuses for the primary and every replica. """ replicas: float = Field(..., alias='replicas') syncmode: str = Field(..., alias='syncMode') + effectivesyncmode: Optional[str] = Field(default=None, alias='effectiveSyncMode') + syncdegraded: bool = Field(..., alias='syncDegraded') + syncacknowledgements: float = Field(..., alias='syncAcknowledgements') + syncstandbycount: float = Field(..., alias='syncStandbyCount') + syncstateconfirmed: Optional[bool] = Field(default=None, alias='syncStateConfirmed') members: List[DedicatedDatabaseMember] = Field(..., alias='members') diff --git a/appwrite/models/dedicated_database_specification_pricing.py b/appwrite/models/dedicated_database_specification_pricing.py index 43d3b1e7..4c614a5a 100644 --- a/appwrite/models/dedicated_database_specification_pricing.py +++ b/appwrite/models/dedicated_database_specification_pricing.py @@ -15,13 +15,10 @@ class DedicatedDatabaseSpecificationPricing(AppwriteModel): Price per GB of bandwidth above the included amount, per month, in USD. replicarate : float High availability replica price as a fraction of the specification cost. - crossregionreplicarate : float - Cross-region replica price as a fraction of the specification cost. pitrrate : float Point-in-time recovery price as a fraction of the specification cost. """ storageoveragerate: float = Field(..., alias='storageOverageRate') bandwidthoveragerate: float = Field(..., alias='bandwidthOverageRate') replicarate: float = Field(..., alias='replicaRate') - crossregionreplicarate: float = Field(..., alias='crossRegionReplicaRate') pitrrate: float = Field(..., alias='pitrRate') diff --git a/appwrite/models/document.py b/appwrite/models/document.py index 4df880fc..b0ae18f4 100644 --- a/appwrite/models/document.py +++ b/appwrite/models/document.py @@ -1,10 +1,12 @@ from typing import Any, Dict, List, Optional, Union, cast, Generic, TypeVar, Type -from pydantic import Field, PrivateAttr +from pydantic import Field, PrivateAttr, TypeAdapter, model_serializer from .base_model import AppwriteModel T = TypeVar('T') +_PAYLOAD_ADAPTER = TypeAdapter(Dict[str, Any]) + class Document(AppwriteModel, Generic[T]): """ Document @@ -57,24 +59,63 @@ def data(self) -> T: def data(self, value: T) -> None: object.__setattr__(self, '_data', value) - def model_dump(self, **kwargs) -> Dict[str, Any]: - result = super().model_dump(**kwargs) - if hasattr(self, '_data'): - if isinstance(self._data, dict): - result['data'] = self._data - elif hasattr(self._data, 'model_dump'): - result['data'] = self._data.model_dump(**kwargs) - else: - result['data'] = self._data - return result + def _serialize_data(self, info, include=None, exclude=None): + if hasattr(self._data, 'model_dump'): + return self._data.model_dump( + mode=info.mode, + by_alias=info.by_alias, + exclude_unset=info.exclude_unset, + exclude_defaults=info.exclude_defaults, + exclude_none=info.exclude_none, + include=include, + exclude=exclude, + ) + + if isinstance(self._data, dict) and (include is not None or exclude is not None): + return _PAYLOAD_ADAPTER.dump_python( + self._data, + mode=info.mode, + by_alias=info.by_alias, + exclude_unset=info.exclude_unset, + exclude_defaults=info.exclude_defaults, + exclude_none=info.exclude_none, + include=include, + exclude=exclude, + ) + + return self._data + + @staticmethod + def _select_data(selector): + """ + Resolves a pydantic include/exclude selector against the 'data' key, which is + serialized here rather than declared as a field. Returns whether the key was + named, and any nested selector to apply within it. + """ + if selector is None: + return False, None + + if isinstance(selector, dict): + if 'data' not in selector: + return False, None + + nested = selector['data'] + + return True, nested if isinstance(nested, (dict, set, frozenset, list, tuple)) else None + + return 'data' in selector, None + + @model_serializer(mode='wrap') + def _serialize_model(self, handler, info): + result = handler(self) + included, include_fields = self._select_data(info.include) + excluded, exclude_fields = self._select_data(info.exclude) + + if info.include is not None and not included: + return result + + if excluded and exclude_fields is None: + return result - def to_dict(self) -> Dict[str, Any]: - result = super().to_dict() - if hasattr(self, '_data'): - if isinstance(self._data, dict): - result['data'] = self._data - elif hasattr(self._data, 'model_dump'): - result['data'] = self._data.model_dump(mode='json') - else: - result['data'] = self._data + result['data'] = self._serialize_data(info, include_fields, exclude_fields) return result diff --git a/appwrite/models/document_list.py b/appwrite/models/document_list.py index a73bb75c..728c8039 100644 --- a/appwrite/models/document_list.py +++ b/appwrite/models/document_list.py @@ -26,7 +26,7 @@ def with_data(cls, data: Dict[str, Any], model_type: Type[T] = dict) -> 'Documen instance = cls.model_validate(data) if 'documents' in data and data['documents'] is not None: instance.documents = [ - Document.with_data(row, model_type) + Document.with_data(row, model_type) for row in data['documents'] ] return instance diff --git a/appwrite/models/embedding.py b/appwrite/models/embedding.py new file mode 100644 index 00000000..9da9bab5 --- /dev/null +++ b/appwrite/models/embedding.py @@ -0,0 +1,24 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel + +class Embedding(AppwriteModel): + """ + Embedding + + Attributes + ---------- + model : str + Embedding model used to generate embeddings. + dimension : float + Number of dimensions for each embedding vector. + embedding : List[Any] + Embedding vector values. If an error occurs, this will be an empty array. + error : str + Error message if embedding generation fails. Empty string if no error. + """ + model: str = Field(..., alias='model') + dimension: float = Field(..., alias='dimension') + embedding: List[Any] = Field(..., alias='embedding') + error: str = Field(..., alias='error') diff --git a/appwrite/models/embedding_list.py b/appwrite/models/embedding_list.py new file mode 100644 index 00000000..d8f437e7 --- /dev/null +++ b/appwrite/models/embedding_list.py @@ -0,0 +1,19 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel +from .embedding import Embedding + +class EmbeddingList(AppwriteModel): + """ + Embedding list + + Attributes + ---------- + total : float + Total number of embeddings that matched your query. + embeddings : List[Embedding] + List of embeddings. + """ + total: float = Field(..., alias='total') + embeddings: List[Embedding] = Field(..., alias='embeddings') diff --git a/appwrite/models/file.py b/appwrite/models/file.py index a49ee806..3657b451 100644 --- a/appwrite/models/file.py +++ b/appwrite/models/file.py @@ -21,6 +21,10 @@ class File(AppwriteModel): File permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). name : str File name. + folder : str + Virtual folder containing the file, with a trailing slash. Empty for the bucket root. + key : str + Full virtual path of the file: the folder followed by the file name. signature : str File MD5 signature. mimetype : str @@ -44,6 +48,8 @@ class File(AppwriteModel): updatedat: str = Field(..., alias='$updatedAt') permissions: List[Any] = Field(..., alias='$permissions') name: str = Field(..., alias='name') + folder: str = Field(..., alias='folder') + key: str = Field(..., alias='key') signature: str = Field(..., alias='signature') mimetype: str = Field(..., alias='mimeType') sizeoriginal: float = Field(..., alias='sizeOriginal') diff --git a/appwrite/models/mfa_challenge_secret.py b/appwrite/models/mfa_challenge_secret.py new file mode 100644 index 00000000..89beb284 --- /dev/null +++ b/appwrite/models/mfa_challenge_secret.py @@ -0,0 +1,27 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel + +class MfaChallengeSecret(AppwriteModel): + """ + MFA Challenge Secret + + Attributes + ---------- + id : str + Token ID. + createdat : str + Token creation date in ISO 8601 format. + userid : str + User ID. + expire : str + Token expiration date in ISO 8601 format. + code : str + Challenge code to be delivered to the end user through a custom channel. + """ + id: str = Field(..., alias='$id') + createdat: str = Field(..., alias='$createdAt') + userid: str = Field(..., alias='userId') + expire: str = Field(..., alias='expire') + code: str = Field(..., alias='code') diff --git a/appwrite/models/mfa_factors.py b/appwrite/models/mfa_factors.py index 1392c1d2..2d6953a1 100644 --- a/appwrite/models/mfa_factors.py +++ b/appwrite/models/mfa_factors.py @@ -17,8 +17,11 @@ class MfaFactors(AppwriteModel): Can email be used for MFA challenge for this account. recoverycode : bool Can recovery code be used for MFA challenge for this account. + custom : bool + Can custom factor be used for MFA challenge for this account. """ totp: bool = Field(..., alias='totp') phone: bool = Field(..., alias='phone') email: bool = Field(..., alias='email') recoverycode: bool = Field(..., alias='recoveryCode') + custom: bool = Field(..., alias='custom') diff --git a/appwrite/models/organization.py b/appwrite/models/organization.py index 95f4bdef..604be7d5 100644 --- a/appwrite/models/organization.py +++ b/appwrite/models/organization.py @@ -26,8 +26,8 @@ class Organization(AppwriteModel, Generic[T]): Total number of team members. prefs : Preferences[T] Team preferences as a key-value object - billingbudget : float - Project budget limit + billingbudget : Optional[float] + Project budget limit. Null when no budget is set. budgetalerts : List[Any] Project budget limit billingplan : str @@ -44,7 +44,7 @@ class Organization(AppwriteModel, Generic[T]): Current invoice cycle start date. billingnextinvoicedate : str Next invoice cycle start date. - billingtrialstartdate : str + billingtrialstartdate : Optional[str] Start date of trial. billingtrialdays : float Number of trial days. @@ -54,29 +54,29 @@ class Organization(AppwriteModel, Generic[T]): Current active aggregation id. paymentmethodid : str Default payment method. - billingaddressid : str + billingaddressid : Optional[str] Default payment method. - backuppaymentmethodid : str + backuppaymentmethodid : Optional[str] Backup payment method. status : str Team status. - remarks : str + remarks : Optional[str] Remarks on team status. - agreementbaa : str + agreementbaa : Optional[str] Organization agreements - programmanagername : str + programmanagername : Optional[str] Program manager's name. - programmanagercalendar : str + programmanagercalendar : Optional[str] Program manager's calendar link. - programdiscordchannelname : str + programdiscordchannelname : Optional[str] Program's discord channel name. - programdiscordchannelurl : str + programdiscordchannelurl : Optional[str] Program's discord channel URL. billinglimits : Optional[BillingLimits] Billing limits reached - billingplandowngrade : str + billingplandowngrade : Optional[str] Billing plan selected for downgrade. - billingtaxid : str + billingtaxid : Optional[str] Tax Id markedfordeletion : bool Marked for deletion @@ -91,7 +91,7 @@ class Organization(AppwriteModel, Generic[T]): name: str = Field(..., alias='name') total: float = Field(..., alias='total') prefs: Preferences[T] = Field(..., alias='prefs') - billingbudget: float = Field(..., alias='billingBudget') + billingbudget: Optional[float] = Field(default=None, alias='billingBudget') budgetalerts: List[Any] = Field(..., alias='budgetAlerts') billingplan: str = Field(..., alias='billingPlan') billingplanid: str = Field(..., alias='billingPlanId') @@ -100,23 +100,23 @@ class Organization(AppwriteModel, Generic[T]): billingstartdate: str = Field(..., alias='billingStartDate') billingcurrentinvoicedate: str = Field(..., alias='billingCurrentInvoiceDate') billingnextinvoicedate: str = Field(..., alias='billingNextInvoiceDate') - billingtrialstartdate: str = Field(..., alias='billingTrialStartDate') + billingtrialstartdate: Optional[str] = Field(default=None, alias='billingTrialStartDate') billingtrialdays: float = Field(..., alias='billingTrialDays') billingaggregationid: str = Field(..., alias='billingAggregationId') billinginvoiceid: str = Field(..., alias='billingInvoiceId') paymentmethodid: str = Field(..., alias='paymentMethodId') - billingaddressid: str = Field(..., alias='billingAddressId') - backuppaymentmethodid: str = Field(..., alias='backupPaymentMethodId') + billingaddressid: Optional[str] = Field(default=None, alias='billingAddressId') + backuppaymentmethodid: Optional[str] = Field(default=None, alias='backupPaymentMethodId') status: str = Field(..., alias='status') - remarks: str = Field(..., alias='remarks') - agreementbaa: str = Field(..., alias='agreementBAA') - programmanagername: str = Field(..., alias='programManagerName') - programmanagercalendar: str = Field(..., alias='programManagerCalendar') - programdiscordchannelname: str = Field(..., alias='programDiscordChannelName') - programdiscordchannelurl: str = Field(..., alias='programDiscordChannelUrl') + remarks: Optional[str] = Field(default=None, alias='remarks') + agreementbaa: Optional[str] = Field(default=None, alias='agreementBAA') + programmanagername: Optional[str] = Field(default=None, alias='programManagerName') + programmanagercalendar: Optional[str] = Field(default=None, alias='programManagerCalendar') + programdiscordchannelname: Optional[str] = Field(default=None, alias='programDiscordChannelName') + programdiscordchannelurl: Optional[str] = Field(default=None, alias='programDiscordChannelUrl') billinglimits: Optional[BillingLimits] = Field(default=None, alias='billingLimits') - billingplandowngrade: str = Field(..., alias='billingPlanDowngrade') - billingtaxid: str = Field(..., alias='billingTaxId') + billingplandowngrade: Optional[str] = Field(default=None, alias='billingPlanDowngrade') + billingtaxid: Optional[str] = Field(default=None, alias='billingTaxId') markedfordeletion: bool = Field(..., alias='markedForDeletion') platform: str = Field(..., alias='platform') projects: List[Any] = Field(..., alias='projects') diff --git a/appwrite/models/policy_list.py b/appwrite/models/policy_list.py index 3e579817..f00999b3 100644 --- a/appwrite/models/policy_list.py +++ b/appwrite/models/policy_list.py @@ -12,6 +12,7 @@ from .policy_session_limit import PolicySessionLimit from .policy_user_limit import PolicyUserLimit from .policy_membership_privacy import PolicyMembershipPrivacy +from .policy_mfa_factors import PolicyMfaFactors from .policy_deny_aliased_email import PolicyDenyAliasedEmail from .policy_deny_disposable_email import PolicyDenyDisposableEmail from .policy_deny_free_email import PolicyDenyFreeEmail @@ -25,8 +26,8 @@ class PolicyList(AppwriteModel): ---------- total : float Total number of policies in the given project. - policies : List[Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail]] + policies : List[Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyMfaFactors, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail]] List of policies. """ total: float = Field(..., alias='total') - policies: List[Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail]] = Field(..., alias='policies') + policies: List[Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyMfaFactors, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail]] = Field(..., alias='policies') diff --git a/appwrite/models/policy_mfa_factors.py b/appwrite/models/policy_mfa_factors.py new file mode 100644 index 00000000..8ff59740 --- /dev/null +++ b/appwrite/models/policy_mfa_factors.py @@ -0,0 +1,27 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel + +class PolicyMfaFactors(AppwriteModel): + """ + Policy MFA Factors + + Attributes + ---------- + id : str + Policy ID. + totp : bool + Whether TOTP can be used to complete an MFA challenge. + email : bool + Whether email can be used to complete an MFA challenge. + phone : bool + Whether phone (SMS) can be used to complete an MFA challenge. + custom : bool + Whether the custom factor can be used to complete an MFA challenge. + """ + id: str = Field(..., alias='$id') + totp: bool = Field(..., alias='totp') + email: bool = Field(..., alias='email') + phone: bool = Field(..., alias='phone') + custom: bool = Field(..., alias='custom') diff --git a/appwrite/models/preferences.py b/appwrite/models/preferences.py index 73a68a21..5d655bd1 100644 --- a/appwrite/models/preferences.py +++ b/appwrite/models/preferences.py @@ -1,10 +1,12 @@ from typing import Any, Dict, List, Optional, Union, cast, Generic, TypeVar, Type -from pydantic import Field, PrivateAttr +from pydantic import Field, PrivateAttr, TypeAdapter, model_serializer from .base_model import AppwriteModel T = TypeVar('T') +_PAYLOAD_ADAPTER = TypeAdapter(Dict[str, Any]) + class Preferences(AppwriteModel, Generic[T]): """ Preferences @@ -29,24 +31,37 @@ def data(self) -> T: def data(self, value: T) -> None: object.__setattr__(self, '_data', value) - def model_dump(self, **kwargs) -> Dict[str, Any]: - result = super().model_dump(**kwargs) - if hasattr(self, '_data'): - if isinstance(self._data, dict): - result['data'] = self._data - elif hasattr(self._data, 'model_dump'): - result['data'] = self._data.model_dump(**kwargs) - else: - result['data'] = self._data - return result - - def to_dict(self) -> Dict[str, Any]: - result = super().to_dict() - if hasattr(self, '_data'): - if isinstance(self._data, dict): - result['data'] = self._data - elif hasattr(self._data, 'model_dump'): - result['data'] = self._data.model_dump(mode='json') - else: - result['data'] = self._data - return result + def _serialize_data(self, info, include=None, exclude=None): + if hasattr(self._data, 'model_dump'): + return self._data.model_dump( + mode=info.mode, + by_alias=info.by_alias, + exclude_unset=info.exclude_unset, + exclude_defaults=info.exclude_defaults, + exclude_none=info.exclude_none, + include=include, + exclude=exclude, + ) + + if isinstance(self._data, dict) and (include is not None or exclude is not None): + return _PAYLOAD_ADAPTER.dump_python( + self._data, + mode=info.mode, + by_alias=info.by_alias, + exclude_unset=info.exclude_unset, + exclude_defaults=info.exclude_defaults, + exclude_none=info.exclude_none, + include=include, + exclude=exclude, + ) + + return self._data + + @model_serializer(mode='wrap') + def _serialize_model(self, handler, info): + result = handler(self) + data = self._serialize_data(info, info.include, info.exclude) + if isinstance(result, dict) and isinstance(data, dict): + return {**result, **data} + + return data diff --git a/appwrite/models/project.py b/appwrite/models/project.py index 76822354..bc9abb12 100644 --- a/appwrite/models/project.py +++ b/appwrite/models/project.py @@ -81,6 +81,8 @@ class Project(AppwriteModel): OAuth2 server allowed scopes oauth2serverdefaultscopes : Optional[List[Any]] OAuth2 server scopes used when an authorization request omits the scope parameter + oauth2serverinstallationscopes : Optional[List[Any]] + Scopes an application may request when installed on a team oauth2serverauthorizationdetailstypes : Optional[List[Any]] OAuth2 server accepted RFC 9396 authorization_details types oauth2serveraccesstokenduration : Optional[float] @@ -139,6 +141,7 @@ class Project(AppwriteModel): oauth2serverauthorizationurl: Optional[str] = Field(default=None, alias='oAuth2ServerAuthorizationUrl') oauth2serverscopes: Optional[List[Any]] = Field(default=None, alias='oAuth2ServerScopes') oauth2serverdefaultscopes: Optional[List[Any]] = Field(default=None, alias='oAuth2ServerDefaultScopes') + oauth2serverinstallationscopes: Optional[List[Any]] = Field(default=None, alias='oAuth2ServerInstallationScopes') oauth2serverauthorizationdetailstypes: Optional[List[Any]] = Field(default=None, alias='oAuth2ServerAuthorizationDetailsTypes') oauth2serveraccesstokenduration: Optional[float] = Field(default=None, alias='oAuth2ServerAccessTokenDuration') oauth2serverrefreshtokenduration: Optional[float] = Field(default=None, alias='oAuth2ServerRefreshTokenDuration') diff --git a/appwrite/models/proxy_invalidation.py b/appwrite/models/proxy_invalidation.py new file mode 100644 index 00000000..511a0feb --- /dev/null +++ b/appwrite/models/proxy_invalidation.py @@ -0,0 +1,24 @@ +from typing import Any, Dict, List, Optional, Union, cast +from pydantic import Field, PrivateAttr + +from .base_model import AppwriteModel + +class ProxyInvalidation(AppwriteModel): + """ + Invalidation + + Attributes + ---------- + domain : str + Domain name. + type : str + Invalidation type. Possible values are "tag", "path", or "all". + reference : str + Invalidated reference. Depending on type this is a cache tag name, a URL path, or empty when type is all. + status : str + Invalidation status. + """ + domain: str = Field(..., alias='domain') + type: str = Field(..., alias='type') + reference: str = Field(..., alias='reference') + status: str = Field(..., alias='status') diff --git a/appwrite/models/row.py b/appwrite/models/row.py index e6e15976..f421cd17 100644 --- a/appwrite/models/row.py +++ b/appwrite/models/row.py @@ -1,10 +1,12 @@ from typing import Any, Dict, List, Optional, Union, cast, Generic, TypeVar, Type -from pydantic import Field, PrivateAttr +from pydantic import Field, PrivateAttr, TypeAdapter, model_serializer from .base_model import AppwriteModel T = TypeVar('T') +_PAYLOAD_ADAPTER = TypeAdapter(Dict[str, Any]) + class Row(AppwriteModel, Generic[T]): """ Row @@ -57,24 +59,63 @@ def data(self) -> T: def data(self, value: T) -> None: object.__setattr__(self, '_data', value) - def model_dump(self, **kwargs) -> Dict[str, Any]: - result = super().model_dump(**kwargs) - if hasattr(self, '_data'): - if isinstance(self._data, dict): - result['data'] = self._data - elif hasattr(self._data, 'model_dump'): - result['data'] = self._data.model_dump(**kwargs) - else: - result['data'] = self._data - return result + def _serialize_data(self, info, include=None, exclude=None): + if hasattr(self._data, 'model_dump'): + return self._data.model_dump( + mode=info.mode, + by_alias=info.by_alias, + exclude_unset=info.exclude_unset, + exclude_defaults=info.exclude_defaults, + exclude_none=info.exclude_none, + include=include, + exclude=exclude, + ) + + if isinstance(self._data, dict) and (include is not None or exclude is not None): + return _PAYLOAD_ADAPTER.dump_python( + self._data, + mode=info.mode, + by_alias=info.by_alias, + exclude_unset=info.exclude_unset, + exclude_defaults=info.exclude_defaults, + exclude_none=info.exclude_none, + include=include, + exclude=exclude, + ) + + return self._data + + @staticmethod + def _select_data(selector): + """ + Resolves a pydantic include/exclude selector against the 'data' key, which is + serialized here rather than declared as a field. Returns whether the key was + named, and any nested selector to apply within it. + """ + if selector is None: + return False, None + + if isinstance(selector, dict): + if 'data' not in selector: + return False, None + + nested = selector['data'] + + return True, nested if isinstance(nested, (dict, set, frozenset, list, tuple)) else None + + return 'data' in selector, None + + @model_serializer(mode='wrap') + def _serialize_model(self, handler, info): + result = handler(self) + included, include_fields = self._select_data(info.include) + excluded, exclude_fields = self._select_data(info.exclude) + + if info.include is not None and not included: + return result + + if excluded and exclude_fields is None: + return result - def to_dict(self) -> Dict[str, Any]: - result = super().to_dict() - if hasattr(self, '_data'): - if isinstance(self._data, dict): - result['data'] = self._data - elif hasattr(self._data, 'model_dump'): - result['data'] = self._data.model_dump(mode='json') - else: - result['data'] = self._data + result['data'] = self._serialize_data(info, include_fields, exclude_fields) return result diff --git a/appwrite/models/row_list.py b/appwrite/models/row_list.py index 86ca37ad..b4508b2a 100644 --- a/appwrite/models/row_list.py +++ b/appwrite/models/row_list.py @@ -26,7 +26,7 @@ def with_data(cls, data: Dict[str, Any], model_type: Type[T] = dict) -> 'RowList instance = cls.model_validate(data) if 'rows' in data and data['rows'] is not None: instance.rows = [ - Row.with_data(row, model_type) + Row.with_data(row, model_type) for row in data['rows'] ] return instance diff --git a/appwrite/models/team_list.py b/appwrite/models/team_list.py index 81602c9b..85610b5c 100644 --- a/appwrite/models/team_list.py +++ b/appwrite/models/team_list.py @@ -26,7 +26,7 @@ def with_data(cls, data: Dict[str, Any], model_type: Type[T] = dict) -> 'TeamLis instance = cls.model_validate(data) if 'teams' in data and data['teams'] is not None: instance.teams = [ - Team.with_data(row, model_type) + Team.with_data(row, model_type) for row in data['teams'] ] return instance diff --git a/appwrite/models/usage_billing_plan.py b/appwrite/models/usage_billing_plan.py index a078e726..40016c29 100644 --- a/appwrite/models/usage_billing_plan.py +++ b/appwrite/models/usage_billing_plan.py @@ -14,13 +14,13 @@ class UsageBillingPlan(AppwriteModel): Bandwidth additional resources executions : AdditionalResource Executions additional resources - member : AdditionalResource + member : Optional[AdditionalResource] Member additional resources realtime : AdditionalResource Realtime additional resources realtimemessages : AdditionalResource Realtime messages additional resources - realtimebandwidth : AdditionalResource + realtimebandwidth : Optional[AdditionalResource] Realtime bandwidth additional resources storage : AdditionalResource Storage additional resources @@ -30,17 +30,17 @@ class UsageBillingPlan(AppwriteModel): GBHour additional resources imagetransformations : AdditionalResource Image transformation additional resources - credits : AdditionalResource + credits : Optional[AdditionalResource] Credits additional resources """ bandwidth: AdditionalResource = Field(..., alias='bandwidth') executions: AdditionalResource = Field(..., alias='executions') - member: AdditionalResource = Field(..., alias='member') + member: Optional[AdditionalResource] = Field(default=None, alias='member') realtime: AdditionalResource = Field(..., alias='realtime') realtimemessages: AdditionalResource = Field(..., alias='realtimeMessages') - realtimebandwidth: AdditionalResource = Field(..., alias='realtimeBandwidth') + realtimebandwidth: Optional[AdditionalResource] = Field(default=None, alias='realtimeBandwidth') storage: AdditionalResource = Field(..., alias='storage') users: AdditionalResource = Field(..., alias='users') gbhours: AdditionalResource = Field(..., alias='GBHours') imagetransformations: AdditionalResource = Field(..., alias='imageTransformations') - credits: AdditionalResource = Field(..., alias='credits') + credits: Optional[AdditionalResource] = Field(default=None, alias='credits') diff --git a/appwrite/models/user_list.py b/appwrite/models/user_list.py index c6c2e627..fa071b9c 100644 --- a/appwrite/models/user_list.py +++ b/appwrite/models/user_list.py @@ -26,7 +26,7 @@ def with_data(cls, data: Dict[str, Any], model_type: Type[T] = dict) -> 'UserLis instance = cls.model_validate(data) if 'users' in data and data['users'] is not None: instance.users = [ - User.with_data(row, model_type) + User.with_data(row, model_type) for row in data['users'] ] return instance diff --git a/appwrite/service.py b/appwrite/service.py index dcf43a08..fef16a05 100644 --- a/appwrite/service.py +++ b/appwrite/service.py @@ -46,5 +46,6 @@ def _parse_response( return model.model_validate(response) except ValidationError as error: raise AppwriteException( - f'Unable to parse response into {model.__name__}: {error}' + f'Unable to parse response into {model.__name__}: {error}', + response=response, ) from error diff --git a/appwrite/services/account.py b/appwrite/services/account.py index 492d9102..ee1f0f96 100644 --- a/appwrite/services/account.py +++ b/appwrite/services/account.py @@ -9,7 +9,6 @@ from ..models.oauth2_consent_token_list import Oauth2ConsentTokenList from ..models.oauth2_consent_token import Oauth2ConsentToken from ..models.identity_list import IdentityList -from ..models.jwt import Jwt from ..models.log_list import LogList from ..enums.authenticator_type import AuthenticatorType from ..models.mfa_type import MfaType @@ -525,44 +524,6 @@ def delete_identity( return response - def create_jwt( - self, - duration: Optional[float] = None - ) -> Jwt: - """ - Use this endpoint to create a JSON Web Token. You can use the resulting JWT to authenticate on behalf of the current user when working with the Appwrite server-side API and SDKs. The JWT secret is valid for 15 minutes from its creation and will be invalid if the user will logout in that time frame. - - Parameters - ---------- - duration : Optional[float] - Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds. - - Returns - ------- - Jwt - API response as a typed Pydantic model - - Raises - ------ - AppwriteException - If API request fails - """ - - api_path = '/account/jwts' - api_params = {} - - if duration is not None: - api_params['duration'] = self._normalize_value(duration) - - 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=Jwt) - - def list_logs( self, queries: Optional[List[str]] = None, @@ -789,7 +750,7 @@ def create_mfa_challenge( Parameters ---------- factor : AuthenticationFactor - Factor used for verification. Must be one of following: `email`, `phone`, `totp`, `recoveryCode`. + Factor used for verification. Must be one of following: `email`, `phone`, `totp`, `recoveryCode`, `custom`. Returns ------- diff --git a/appwrite/services/activities.py b/appwrite/services/activities.py index 0d1e5b5b..a534716d 100644 --- a/appwrite/services/activities.py +++ b/appwrite/services/activities.py @@ -13,14 +13,14 @@ def __init__(self, client) -> None: def list_events( self, - queries: Optional[str] = None + queries: Optional[List[str]] = None ) -> ActivityEventList: """ List all events for selected filters. Parameters ---------- - queries : Optional[str] + 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/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on attributes such as userId, teamId, etc. Returns diff --git a/appwrite/services/apps.py b/appwrite/services/apps.py index d4a3f681..bd0e6f90 100644 --- a/appwrite/services/apps.py +++ b/appwrite/services/apps.py @@ -262,7 +262,7 @@ def get( Parameters ---------- app_id : str - Application unique ID or HTTPS client ID metadata document URL. + Application unique ID. Returns ------- @@ -356,7 +356,7 @@ def update( device_flow : Optional[bool] Allow this client to use the OAuth2 Device Authorization Grant (RFC 8628) for input-constrained devices such as TVs and CLIs. Defaults to false. installation_scopes : Optional[List[str]] - Scopes the application requests when installed on a team. Organization-level and project-level scopes only; use the list scopes endpoint with `type=installation` to discover available values. Maximum of 100 scopes are allowed. + Scopes the application requests when installed on a team. Only scopes allowed by the project's OAuth2 server installation scopes configuration are accepted; use the list installation scopes endpoint to discover available values. Maximum of 100 scopes are allowed. installation_redirect_url : Optional[str] URL users are redirected to after creating or updating an installation of this application. Must be an https URL, an http loopback URL (localhost, 127.0.0.1, [::1]), or a private-use scheme URI, and must not contain a fragment. Leave empty for no redirect. @@ -475,7 +475,7 @@ def list_installations( total: Optional[bool] = None ) -> AppInstallationList: """ - List installations of an application. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header. + List installations of an application. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header, or a caller with update access to the app. Parameters ---------- @@ -523,7 +523,7 @@ def get_installation( installation_id: str ) -> AppInstallation: """ - Get an installation of an application by its unique ID. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header. + Get an installation of an application by its unique ID. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header, or a caller with update access to the app. Parameters ---------- @@ -563,13 +563,60 @@ def get_installation( return self._parse_response(response, model=AppInstallation) + def delete_installation( + self, + app_id: str, + installation_id: str + ) -> Dict[str, Any]: + """ + Delete an installation of an application by its unique ID. Requires a caller with update access to the app. Previously issued installation access tokens are revoked. + + Parameters + ---------- + app_id : str + Application unique ID. + installation_id : str + Installation unique ID. + + Returns + ------- + Dict[str, Any] + API response as a dictionary + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/apps/{appId}/installations/{installationId}' + api_params = {} + if app_id is None: + raise AppwriteException('Missing required parameter: "app_id"') + + if installation_id is None: + raise AppwriteException('Missing required parameter: "installation_id"') + + api_path = api_path.replace('{appId}', str(self._normalize_value(app_id))) + api_path = api_path.replace('{installationId}', str(self._normalize_value(installation_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 create_installation_token( self, app_id: str, installation_id: str ) -> Oauth2Token: """ - Create a token for an installation of an application. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header. The returned token carries the scopes and authorization details granted to the installation, and can be used as an `Authorization: Bearer` header everywhere OAuth2 access tokens are accepted. Multiple tokens can be active for the same installation at once; each token stays valid until it expires or the installation is updated or deleted. + Create a token for an installation of an application. Requires an app key sent in the `X-Appwrite-Key` header alongside the `X-Appwrite-App` header, or a caller with update access to the app. The returned token carries the scopes and authorization details granted to the installation, and can be used as an `Authorization: Bearer` header everywhere OAuth2 access tokens are accepted. Multiple tokens can be active for the same installation at once; each token stays valid until it expires or the installation is updated or deleted. Parameters ---------- diff --git a/appwrite/services/backups.py b/appwrite/services/backups.py index c8b782c5..cc41ff09 100644 --- a/appwrite/services/backups.py +++ b/appwrite/services/backups.py @@ -439,13 +439,13 @@ def create_restoration( """ Create and trigger a new restoration for a backup on a project. - For a backup of one database, the restoration resolves its destination before it is queued. Pass `newResourceId` to restore into that database ID, including the archived database ID to overwrite it. When `newResourceId` is omitted, a new database ID is generated and returned in `options`. + For a backup of one database, the restoration resolves its destination before it is queued. When `newResourceId` is omitted, the archived database is restored in place and its own ID is returned in `options`. Pass a different `newResourceId` to restore alongside it as a new database instead. The restoration migration records the archived database in `resourceId` and `resourceType`, and the resolved database in `destinationResourceId` and `destinationResourceType`. Database types are stored canonically as `database`, `documentsdb`, or `vectorsdb`. Project-wide restorations leave these fields empty because they do not have a single source or destination database. To list every migration related to one database, use its canonical type in a nested `OR(AND(...), AND(...), AND(...))` across the root, parent, and destination relation pairs: `(resourceType, resourceId)`, `(parentResourceType, parentResourceId)`, and `(destinationResourceType, destinationResourceId)`. Legacy and TablesDB databases use `database`; the operational `resourceType` of a table migration is not rewritten to `tablesdb`. - When restoring a DocumentsDB or VectorsDB database to a new resource from a dedicated source, the restore provisions a fresh dedicated backing database at the source database's own specification. + When restoring a DocumentsDB or VectorsDB database from a dedicated source, the restore provisions a fresh dedicated backing database at the source database's own specification and lands the data there. An in-place restore swaps the database onto that backing only once the restore has succeeded, and retires the backing it displaced only once that swap is confirmed, so the source keeps serving its own data until the restored data is in place and any failure leaves it untouched. A serverless source has no dedicated backing to clone and restores onto the archived database instead. Parameters @@ -455,7 +455,7 @@ def create_restoration( services : List[BackupServices] Array of services to restore new_resource_id : Optional[str] - Destination resource ID. Omit to generate a new ID, or pass the archived resource ID to overwrite it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. + Destination resource ID. Omit to restore the archived resource in place, or pass a different ID to restore alongside it as a new resource. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. new_resource_name : Optional[str] Database name. Max length: 128 chars. diff --git a/appwrite/services/databases.py b/appwrite/services/databases.py index 51831aec..32c63923 100644 --- a/appwrite/services/databases.py +++ b/appwrite/services/databases.py @@ -642,7 +642,7 @@ def create_collection( enabled : Optional[bool] Is collection enabled? When set to 'disabled', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled. attributes : Optional[List[Dict[str, Any]]] - Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options. + Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, varchar, text, mediumtext, longtext, integer, bigint, double, boolean, datetime, point, linestring, polygon, email, url, ip, enum), size (integer, required for string and varchar types), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options. indexes : Optional[List[Dict[str, Any]]] Array of index definitions to create. Each index should contain: key (string), type (string: key, fulltext, unique, spatial), attributes (array of attribute keys), orders (array of ASC/DESC, optional), and lengths (array of integers, optional). @@ -2951,7 +2951,7 @@ def create_relationship_attribute( related_collection_id : str Related Collection ID. type : RelationshipType - Relation type + Relationship type. Possible values are: oneToOne, oneToMany, manyToOne, manyToMany. two_way : Optional[bool] Is Two Way? key : Optional[str] @@ -2959,7 +2959,7 @@ def create_relationship_attribute( two_way_key : Optional[str] Two Way Attribute Key. on_delete : Optional[RelationMutate] - Constraints option + Delete constraint. Possible values are: cascade, restrict, setNull. Returns ------- @@ -3033,7 +3033,7 @@ def update_relationship_attribute( key : str Attribute Key. on_delete : Optional[RelationMutate] - Constraints option + Delete constraint. Possible values are: cascade, restrict, setNull. new_key : Optional[str] New Attribute Key. diff --git a/appwrite/services/embeddings.py b/appwrite/services/embeddings.py new file mode 100644 index 00000000..50848d06 --- /dev/null +++ b/appwrite/services/embeddings.py @@ -0,0 +1,58 @@ +from ..service import Service +from urllib.parse import quote +from typing import Any, Dict, List, Optional, Union +from ..exception import AppwriteException +from appwrite.utils.deprecated import deprecated +from ..enums.embedding_model import EmbeddingModel +from ..models.embedding_list import EmbeddingList + +class Embeddings(Service): + + def __init__(self, client) -> None: + super(Embeddings, self).__init__(client) + + def create_text_embeddings( + self, + texts: List[str], + model: Optional[EmbeddingModel] = None + ) -> EmbeddingList: + """ + Generate vector embeddings for an array of text using the selected embedding model. Use the returned vectors to power semantic search and similarity queries against your vector collections. + + + Parameters + ---------- + texts : List[str] + Array of text to generate embeddings. + model : Optional[EmbeddingModel] + The embedding model to use for generating vector embeddings. + + Returns + ------- + EmbeddingList + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/embeddings/text' + api_params = {} + if texts is None: + raise AppwriteException('Missing required parameter: "texts"') + + + api_params['texts'] = self._normalize_value(texts) + if model is not None: + api_params['model'] = self._normalize_value(model) + + 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=EmbeddingList) + diff --git a/appwrite/services/functions.py b/appwrite/services/functions.py index 4b0f552f..07bd41b4 100644 --- a/appwrite/services/functions.py +++ b/appwrite/services/functions.py @@ -260,7 +260,7 @@ def list_specifications( Parameters ---------- type : Optional[str] - Specification type to list. Can be one of: runtimes, builds. + Specification type to list. Can be one of: runtimes, builds. Defaults to runtimes. Returns ------- diff --git a/appwrite/services/project.py b/appwrite/services/project.py index 38af962f..1665fa81 100644 --- a/appwrite/services/project.py +++ b/appwrite/services/project.py @@ -7,8 +7,8 @@ from ..enums.project_auth_method_id import ProjectAuthMethodId from ..models.key_list import KeyList from ..enums.project_key_scopes import ProjectKeyScopes -from ..models.key import Key from ..models.ephemeral_key import EphemeralKey +from ..models.key import Key from ..models.mock_number_list import MockNumberList from ..models.mock_number import MockNumber from ..models.o_auth2_provider_list import OAuth2ProviderList @@ -74,6 +74,7 @@ from ..models.policy_session_limit import PolicySessionLimit from ..models.policy_user_limit import PolicyUserLimit from ..models.policy_membership_privacy import PolicyMembershipPrivacy +from ..models.policy_mfa_factors import PolicyMfaFactors from ..models.policy_deny_aliased_email import PolicyDenyAliasedEmail from ..models.policy_deny_disposable_email import PolicyDenyDisposableEmail from ..models.policy_deny_free_email import PolicyDenyFreeEmail @@ -237,67 +238,6 @@ def list_keys( return self._parse_response(response, model=KeyList) - def create_key( - self, - key_id: str, - name: str, - scopes: List[ProjectKeyScopes], - expire: Optional[str] = None - ) -> Key: - """ - Create a new API key. It's recommended to have multiple API keys with strict scopes for separate functions within your project. - - You can also create an ephemeral API key if you need a short-lived key instead. - - Parameters - ---------- - key_id : str - Key ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. - name : str - Key name. Max length: 128 chars. - scopes : List[ProjectKeyScopes] - Key scopes list. Maximum of 200 scopes are allowed. - expire : Optional[str] - Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration. - - Returns - ------- - Key - API response as a typed Pydantic model - - Raises - ------ - AppwriteException - If API request fails - """ - - api_path = '/project/keys' - api_params = {} - if key_id is None: - raise AppwriteException('Missing required parameter: "key_id"') - - if name is None: - raise AppwriteException('Missing required parameter: "name"') - - if scopes is None: - raise AppwriteException('Missing required parameter: "scopes"') - - - api_params['keyId'] = self._normalize_value(key_id) - api_params['name'] = self._normalize_value(name) - api_params['scopes'] = self._normalize_value(scopes) - if expire is not None: - api_params['expire'] = self._normalize_value(expire) - - 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=Key) - - def create_ephemeral_key( self, scopes: List[ProjectKeyScopes], @@ -796,7 +736,8 @@ def update_o_auth2_server( user_code_length: Optional[float] = None, user_code_format: Optional[str] = None, device_code_duration: Optional[float] = None, - default_scopes: Optional[List[str]] = None + default_scopes: Optional[List[str]] = None, + installation_scopes: Optional[List[str]] = None ) -> ProjectModel: """ Update the OAuth2 server (OIDC provider) configuration. @@ -833,6 +774,8 @@ def update_o_auth2_server( Lifetime in seconds of device flow device codes and user codes. Device codes are intentionally short-lived. Leave empty to use default 600. default_scopes : Optional[List[str]] 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. Returns ------- @@ -882,6 +825,8 @@ def update_o_auth2_server( api_params['deviceCodeDuration'] = self._normalize_value(device_code_duration) if default_scopes is not None: api_params['defaultScopes'] = self._normalize_value(default_scopes) + if installation_scopes is not None: + api_params['installationScopes'] = self._normalize_value(installation_scopes) response = self.client.call('put', api_path, { 'X-Appwrite-Project': self.client.get_config('project'), @@ -4149,6 +4094,59 @@ def update_membership_privacy_policy( return self._parse_response(response, model=ProjectModel) + def update_mfa_factors_policy( + self, + totp: Optional[bool] = None, + email: Optional[bool] = None, + phone: Optional[bool] = None, + custom: Optional[bool] = None + ) -> ProjectModel: + """ + 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. + + Parameters + ---------- + totp : Optional[bool] + Set to true to allow TOTP to complete an MFA challenge, or false to disable it. + email : Optional[bool] + Set to true to allow email to complete an MFA challenge, or false to disable it. + phone : Optional[bool] + 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. + + Returns + ------- + ProjectModel + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/project/policies/mfa-factors' + api_params = {} + + if totp is not None: + api_params['totp'] = self._normalize_value(totp) + if email is not None: + api_params['email'] = self._normalize_value(email) + if phone is not None: + api_params['phone'] = self._normalize_value(phone) + if custom is not None: + api_params['custom'] = self._normalize_value(custom) + + response = self.client.call('patch', api_path, { + 'X-Appwrite-Project': self.client.get_config('project'), + 'content-type': 'application/json', + 'accept': 'application/json', + }, api_params) + + return self._parse_response(response, model=ProjectModel) + + def update_password_dictionary_policy( self, enabled: bool @@ -4201,7 +4199,7 @@ def update_password_history_policy( Parameters ---------- total : Optional[float] - Set the password history length per user. Value can be between 1 and 5000, or null to disable the limit. + Set the password history length per user. Value can be between 1 and 20, or null to disable the limit. Returns ------- @@ -4376,7 +4374,7 @@ def update_session_duration_policy( Parameters ---------- duration : float - Maximum session length in seconds. Minium allowed value is 5 second, and maximum is 1 year, which is 31536000 seconds. + Maximum session length in seconds. Minium allowed value is 60 seconds, and maximum is 1 year, which is 31536000 seconds. Returns ------- @@ -4448,15 +4446,15 @@ def update_session_invalidation_policy( def update_session_limit_policy( self, - total: Optional[float] + total: float ) -> ProjectModel: """ 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. Parameters ---------- - total : Optional[float] - Set the maximum number of sessions allowed per user. Value can be between 1 and 5000, or null to disable the limit. + total : float + Set the maximum number of sessions allowed per user. Value can be between 1 and 100. Returns ------- @@ -4471,6 +4469,9 @@ def update_session_limit_policy( api_path = '/project/policies/session-limit' api_params = {} + if total is None: + raise AppwriteException('Missing required parameter: "total"') + api_params['total'] = self._normalize_value(total) @@ -4493,7 +4494,7 @@ def update_user_limit_policy( Parameters ---------- total : Optional[float] - Set the maximum number of users allowed in the project. Value can be between 1 and 5000, or null to disable the limit. + 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. Returns ------- @@ -4523,18 +4524,18 @@ def update_user_limit_policy( def get_policy( self, policy_id: ProjectPolicyId - ) -> Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail]: + ) -> Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyMfaFactors, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail]: """ Get a policy by its unique ID. This endpoint returns the current configuration for the requested project policy. Parameters ---------- policy_id : ProjectPolicyId - Policy ID. Can be one of: password-dictionary, password-history, password-strength, password-personal-data, session-alert, session-duration, session-invalidation, session-limit, user-limit, membership-privacy, deny-aliased-email, deny-disposable-email, deny-free-email, deny-corporate-email. + Policy ID. Can be one of: password-dictionary, password-history, password-strength, password-personal-data, 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. Returns ------- - Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail] + Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyMfaFactors, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail] API response as one of the typed response models Raises @@ -4588,6 +4589,9 @@ def get_policy( if response.get('$id') == 'membership-privacy': return self._parse_response(response, model=PolicyMembershipPrivacy) + if response.get('$id') == 'mfa-factors': + return self._parse_response(response, model=PolicyMfaFactors) + if response.get('$id') == 'deny-aliased-email': return self._parse_response(response, model=PolicyDenyAliasedEmail) diff --git a/appwrite/services/proxy.py b/appwrite/services/proxy.py index 44f18747..ce209139 100644 --- a/appwrite/services/proxy.py +++ b/appwrite/services/proxy.py @@ -3,6 +3,8 @@ from typing import Any, Dict, List, Optional, Union from ..exception import AppwriteException from appwrite.utils.deprecated import deprecated +from ..enums.invalidation_type import InvalidationType +from ..models.proxy_invalidation import ProxyInvalidation from ..models.proxy_rule_list import ProxyRuleList from ..models.proxy_rule import ProxyRule from ..enums.status_code import StatusCode @@ -13,6 +15,60 @@ class Proxy(Service): def __init__(self, client) -> None: super(Proxy, self).__init__(client) + def create_invalidation( + self, + domain: str, + type: InvalidationType, + reference: Optional[str] = None + ) -> ProxyInvalidation: + """ + Create a new CDN cache invalidation for a domain. Executes a hard purge of cached content. + + Depending on type, the invalidation purges a single cache tag, a single URL path, or all cached content for the domain. + + Parameters + ---------- + domain : str + Domain name. + type : InvalidationType + Type of reference passed. Allowed values are: tag, path, all + reference : Optional[str] + Reference to invalidate. Depending on type this can be: cache tag name (up to 128 characters), URL path (up to 2048 characters). Not required when type is all. + + Returns + ------- + ProxyInvalidation + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/proxy/invalidations' + api_params = {} + if domain is None: + raise AppwriteException('Missing required parameter: "domain"') + + if type is None: + raise AppwriteException('Missing required parameter: "type"') + + + api_params['domain'] = self._normalize_value(domain) + api_params['type'] = self._normalize_value(type) + if reference is not None: + api_params['reference'] = self._normalize_value(reference) + + 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=ProxyInvalidation) + + def list_rules( self, queries: Optional[List[str]] = None, diff --git a/appwrite/services/sites.py b/appwrite/services/sites.py index bcf0c1ad..8b930f17 100644 --- a/appwrite/services/sites.py +++ b/appwrite/services/sites.py @@ -267,7 +267,7 @@ def list_specifications( Parameters ---------- type : Optional[str] - Specification type to list. Can be one of: runtimes, builds. + Specification type to list. Can be one of: runtimes, builds. Defaults to runtimes. Returns ------- diff --git a/appwrite/services/storage.py b/appwrite/services/storage.py index a450e58b..7511db08 100644 --- a/appwrite/services/storage.py +++ b/appwrite/services/storage.py @@ -341,7 +341,7 @@ def list_files( bucket_id : str Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket). 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, signature, mimeType, sizeOriginal, chunksTotal, chunksUploaded + 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, folder, signature, mimeType, sizeOriginal, chunksTotal, chunksUploaded search : Optional[str] Search term to filter your list results. Max length: 256 chars. total : Optional[bool] @@ -386,6 +386,7 @@ def create_file( file_id: str, file: InputFile, permissions: Optional[List[str]] = None, + folder: Optional[str] = None, on_progress = None ) -> File: """ @@ -408,6 +409,8 @@ def create_file( Binary file. Appwrite SDKs provide helpers to handle file input. [Learn about file input](https://appwrite.io/docs/products/storage/upload-download#input-file). permissions : Optional[List[str]] An array of permission strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions). + folder : Optional[str] + Virtual folder to place the file in, for example "photos/2026". Nest folders with `/`. Defaults to the bucket root. on_progress : callable, optional Optional callback for upload progress @@ -439,6 +442,8 @@ def create_file( api_params['file'] = self._normalize_value(file) if permissions is not None: api_params['permissions'] = self._normalize_value(permissions) + if folder is not None: + api_params['folder'] = self._normalize_value(folder) param_name = 'file' diff --git a/appwrite/services/tables_db.py b/appwrite/services/tables_db.py index f84f9cca..94854111 100644 --- a/appwrite/services/tables_db.py +++ b/appwrite/services/tables_db.py @@ -9,6 +9,9 @@ from ..models.transaction_list import TransactionList from ..models.transaction import Transaction from ..models.dedicated_database import DedicatedDatabase +from ..models.database_migration_list import DatabaseMigrationList +from ..models.database_migration import DatabaseMigration +from ..models.dedicated_database_operation_list import DedicatedDatabaseOperationList from ..models.dedicated_database_replicas import DedicatedDatabaseReplicas from ..models.database_status import DatabaseStatus from ..models.table_list import TableList @@ -101,7 +104,8 @@ def create( name: str, enabled: Optional[bool] = None, specification: Optional[str] = None, - replicas: Optional[float] = None + replicas: Optional[float] = None, + sync_mode: Optional[str] = None ) -> Database: """ Create a new Database. @@ -119,6 +123,8 @@ def create( Database specification. Defaults to `serverless`, which creates the database on the shared pool. Any other value provisions a dedicated database on that specification. replicas : Optional[float] Number of high availability replicas (0-5) for the dedicated database backing this database. Requires a dedicated `specification`; must be 0 for a serverless database. High availability is enabled when greater than 0. + sync_mode : Optional[str] + Replication sync mode for the dedicated database backing this database. Requires a dedicated `specification`; the mode is only in force once there is at least one replica. Allowed values: async, sync, quorum. Returns ------- @@ -148,6 +154,8 @@ def create( api_params['specification'] = self._normalize_value(specification) if replicas is not None: api_params['replicas'] = self._normalize_value(replicas) + if sync_mode is not None: + api_params['syncMode'] = self._normalize_value(sync_mode) response = self.client.call('post', api_path, { 'X-Appwrite-Project': self.client.get_config('project'), @@ -478,7 +486,9 @@ def update( database_id: str, name: Optional[str] = None, enabled: Optional[bool] = None, - replicas: Optional[float] = None + specification: Optional[str] = None, + replicas: Optional[float] = None, + sync_mode: Optional[str] = None ) -> Database: """ Update a database by its unique ID. @@ -491,8 +501,12 @@ def update( Database name. Max length: 128 chars. enabled : Optional[bool] Is database enabled? When set to 'disabled', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled. + specification : Optional[str] + Database specification. Resizing between dedicated specifications changes cpu, memory, storage and the connection ceiling via a rolling cutover with zero downtime. Moving a `serverless` database onto a dedicated specification is a data migration, not a resize. replicas : Optional[float] Number of high availability replicas (0-5) for the dedicated database backing this database. Only valid when the database is backed by a dedicated specification. High availability is enabled when greater than 0. + sync_mode : Optional[str] + Replication sync mode for the dedicated database backing this database. Only valid when the database is backed by a dedicated specification; the mode is only in force once there is at least one replica. Allowed values: async, sync, quorum. Returns ------- @@ -516,8 +530,12 @@ def update( api_params['name'] = self._normalize_value(name) if enabled is not None: api_params['enabled'] = self._normalize_value(enabled) + if specification is not None: + api_params['specification'] = self._normalize_value(specification) if replicas is not None: api_params['replicas'] = self._normalize_value(replicas) + if sync_mode is not None: + api_params['syncMode'] = self._normalize_value(sync_mode) response = self.client.call('put', api_path, { 'X-Appwrite-Project': self.client.get_config('project'), @@ -573,7 +591,7 @@ def create_failover( target_replica_id: Optional[str] = None ) -> DedicatedDatabase: """ - Trigger a manual failover for a dedicated database with high availability enabled. Promotes a replica to primary. The failover runs asynchronously; poll the database document for status updates. + Trigger a manual failover for a dedicated database with high availability enabled. Promotes a replica to primary. The failover runs asynchronously; poll the database document for status updates. A database left mid-operation by a failover that did not finish also accepts this call as a repair, provided `targetReplicaId` names the member to promote. Parameters ---------- @@ -612,6 +630,291 @@ def create_failover( return self._parse_response(response, model=DedicatedDatabase) + def list_migrations( + self, + database_id: str + ) -> DatabaseMigrationList: + """ + List the dedicated migrations for a TablesDB database. A database has at most one in-flight migration. + + Parameters + ---------- + database_id : str + Database ID. + + Returns + ------- + DatabaseMigrationList + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/tablesdb/{databaseId}/migrations' + api_params = {} + if database_id is None: + raise AppwriteException('Missing required parameter: "database_id"') + + api_path = api_path.replace('{databaseId}', str(self._normalize_value(database_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=DatabaseMigrationList) + + + def create_migration( + self, + database_id: str, + specification: str, + auto_cutover: Optional[bool] = None + ) -> DatabaseMigration: + """ + Start migrating a serverless TablesDB database onto a dedicated MySQL compute. Data is copied to the target while the source stays live, with a brief read-only window during cutover. + + Parameters + ---------- + database_id : str + Database ID. + specification : str + Dedicated compute specification to provision as the migration target (e.g. s-2vcpu-4gb). The migration always targets a dedicated compute, so `serverless` is not accepted. + auto_cutover : Optional[bool] + Whether to cut over automatically once the copy is verified. When disabled the migration parks at ready_to_cutover and holds there until the cutover is performed manually. + + Returns + ------- + DatabaseMigration + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/tablesdb/{databaseId}/migrations' + api_params = {} + if database_id is None: + raise AppwriteException('Missing required parameter: "database_id"') + + if specification is None: + raise AppwriteException('Missing required parameter: "specification"') + + api_path = api_path.replace('{databaseId}', str(self._normalize_value(database_id))) + + api_params['specification'] = self._normalize_value(specification) + if auto_cutover is not None: + api_params['autoCutover'] = self._normalize_value(auto_cutover) + + 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=DatabaseMigration) + + + def get_migration( + self, + database_id: str, + migration_id: str + ) -> DatabaseMigration: + """ + Get a single dedicated migration for a TablesDB database by its ID. + + Parameters + ---------- + database_id : str + Database ID. + migration_id : str + Migration ID. + + Returns + ------- + DatabaseMigration + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/tablesdb/{databaseId}/migrations/{migrationId}' + api_params = {} + if database_id is None: + raise AppwriteException('Missing required parameter: "database_id"') + + if migration_id is None: + raise AppwriteException('Missing required parameter: "migration_id"') + + api_path = api_path.replace('{databaseId}', str(self._normalize_value(database_id))) + api_path = api_path.replace('{migrationId}', str(self._normalize_value(migration_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=DatabaseMigration) + + + def delete_migration( + self, + database_id: str, + migration_id: str + ) -> Dict[str, Any]: + """ + Abort an in-flight TablesDB dedicated migration. Only allowed before cutover; once the migration has cut over it cannot be aborted. + + Parameters + ---------- + database_id : str + Database ID. + migration_id : str + Migration ID. + + Returns + ------- + Dict[str, Any] + API response as a dictionary + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/tablesdb/{databaseId}/migrations/{migrationId}' + api_params = {} + if database_id is None: + raise AppwriteException('Missing required parameter: "database_id"') + + if migration_id is None: + raise AppwriteException('Missing required parameter: "migration_id"') + + api_path = api_path.replace('{databaseId}', str(self._normalize_value(database_id))) + api_path = api_path.replace('{migrationId}', str(self._normalize_value(migration_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 cutover_migration( + self, + database_id: str, + migration_id: str + ) -> DatabaseMigration: + """ + Cut a verified TablesDB migration over to its dedicated compute. Only applies to a migration created with `autoCutover` disabled, which waits at `ready_to_cutover` until this is called. The routing flip happens shortly after this returns, with a brief read-only window. One call buys one attempt: a cutover that fails a check returns the migration to `verifying` and parks it again, so call this once more to retry. + + Parameters + ---------- + database_id : str + Database ID. + migration_id : str + Migration ID. + + Returns + ------- + DatabaseMigration + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/tablesdb/{databaseId}/migrations/{migrationId}/cutover' + api_params = {} + if database_id is None: + raise AppwriteException('Missing required parameter: "database_id"') + + if migration_id is None: + raise AppwriteException('Missing required parameter: "migration_id"') + + api_path = api_path.replace('{databaseId}', str(self._normalize_value(database_id))) + api_path = api_path.replace('{migrationId}', str(self._normalize_value(migration_id))) + + + 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=DatabaseMigration) + + + def list_operations( + self, + database_id: str, + status: Optional[str] = None, + limit: Optional[float] = None, + offset: Optional[float] = None + ) -> DedicatedDatabaseOperationList: + """ + List the lifecycle operations recorded for a dedicated database, newest first. Every provision, update, restore, backup and replication action is recorded here with its outcome, including an attempt that was abandoned because another worker took over the database. + + Parameters + ---------- + database_id : str + Database ID. + status : Optional[str] + Filter by operation status. + limit : Optional[float] + Maximum number of operations to return. + offset : Optional[float] + Number of operations to skip. + + Returns + ------- + DedicatedDatabaseOperationList + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/tablesdb/{databaseId}/operations' + api_params = {} + if database_id is None: + raise AppwriteException('Missing required parameter: "database_id"') + + api_path = api_path.replace('{databaseId}', str(self._normalize_value(database_id))) + + if status is not None: + api_params['status'] = self._normalize_value(status) + if limit is not None: + api_params['limit'] = self._normalize_value(limit) + if offset is not None: + api_params['offset'] = self._normalize_value(offset) + + 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=DedicatedDatabaseOperationList) + + def get_replicas( self, database_id: str @@ -773,7 +1076,7 @@ def create_table( enabled : Optional[bool] Is table enabled? When set to 'disabled', users cannot access the table but Server SDKs with and API key can still read and write to the table. No data is lost when this is toggled. columns : Optional[List[Dict[str, Any]]] - Array of column definitions to create. Each column should contain: key (string), type (string: string, integer, float, boolean, datetime, relationship), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options. + Array of column definitions to create. Each column should contain: key (string), type (string: string, varchar, text, mediumtext, longtext, integer, bigint, double, boolean, datetime, point, linestring, polygon, email, url, ip, enum), size (integer, required for string and varchar types), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options. indexes : Optional[List[Dict[str, Any]]] Array of index definitions to create. Each index should contain: key (string), type (string: key, fulltext, unique, spatial), attributes (array of column keys), orders (array of ASC/DESC, optional), and lengths (array of integers, optional). @@ -2988,7 +3291,7 @@ def create_relationship_column( related_table_id : str Related Table ID. type : RelationshipType - Relation type + Relationship type. Possible values are: oneToOne, oneToMany, manyToOne, manyToMany. two_way : Optional[bool] Is Two Way? key : Optional[str] @@ -2996,7 +3299,7 @@ def create_relationship_column( two_way_key : Optional[str] Two Way Column Key. on_delete : Optional[RelationMutate] - Constraints option + Delete constraint. Possible values are: cascade, restrict, setNull. Returns ------- @@ -3822,7 +4125,7 @@ def update_relationship_column( key : str Column Key. on_delete : Optional[RelationMutate] - Constraints option + Delete constraint. Possible values are: cascade, restrict, setNull. new_key : Optional[str] New Column Key. diff --git a/appwrite/services/users.py b/appwrite/services/users.py index 8eab2dc7..fad7bd3c 100644 --- a/appwrite/services/users.py +++ b/appwrite/services/users.py @@ -11,6 +11,7 @@ from ..models.log_list import LogList from ..models.membership_list import MembershipList from ..enums.authenticator_type import AuthenticatorType +from ..models.mfa_challenge_secret import MfaChallengeSecret from ..models.mfa_factors import MfaFactors from ..models.mfa_recovery_codes import MfaRecoveryCodes from ..models.preferences import Preferences @@ -1219,6 +1220,52 @@ def delete_mfa_authenticator( return response + def get_mfa_challenge( + self, + user_id: str, + challenge_id: str + ) -> MfaChallengeSecret: + """ + Get a custom MFA challenge for a user, including the code to be delivered through your own channel. + + Parameters + ---------- + user_id : str + User ID. + challenge_id : str + ID of the challenge. + + Returns + ------- + MfaChallengeSecret + API response as a typed Pydantic model + + Raises + ------ + AppwriteException + If API request fails + """ + + api_path = '/users/{userId}/mfa/challenges/{challengeId}' + api_params = {} + if user_id is None: + raise AppwriteException('Missing required parameter: "user_id"') + + if challenge_id is None: + raise AppwriteException('Missing required parameter: "challenge_id"') + + api_path = api_path.replace('{userId}', str(self._normalize_value(user_id))) + api_path = api_path.replace('{challengeId}', str(self._normalize_value(challenge_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=MfaChallengeSecret) + + def list_mfa_factors( self, user_id: str diff --git a/docs/examples/activities/list-events.md b/docs/examples/activities/list-events.md index bbeb644b..17f3b67d 100644 --- a/docs/examples/activities/list-events.md +++ b/docs/examples/activities/list-events.md @@ -11,7 +11,7 @@ client.set_key('') # Your secret API key activities = Activities(client) result: ActivityEventList = activities.list_events( - queries = '' # optional + queries = [] # optional ) print(result.model_dump()) diff --git a/docs/examples/apps/create-installation-token.md b/docs/examples/apps/create-installation-token.md index 62a62ca6..acbe6cc7 100644 --- a/docs/examples/apps/create-installation-token.md +++ b/docs/examples/apps/create-installation-token.md @@ -6,7 +6,7 @@ from appwrite.models import Oauth2Token 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 +client.set_session('') # The user session to authenticate with apps = Apps(client) diff --git a/docs/examples/account/create-jwt.md b/docs/examples/apps/delete-installation.md similarity index 58% rename from docs/examples/account/create-jwt.md rename to docs/examples/apps/delete-installation.md index a28c65fd..2a941e8e 100644 --- a/docs/examples/account/create-jwt.md +++ b/docs/examples/apps/delete-installation.md @@ -1,18 +1,16 @@ ```python from appwrite.client import Client -from appwrite.services.account import Account -from appwrite.models import Jwt +from appwrite.services.apps import Apps 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) +apps = Apps(client) -result: Jwt = account.create_jwt( - duration = 0 # optional +result = apps.delete_installation( + app_id = '', + installation_id = '' ) - -print(result.model_dump()) ``` diff --git a/docs/examples/apps/get-installation.md b/docs/examples/apps/get-installation.md index f6a52721..d71858c9 100644 --- a/docs/examples/apps/get-installation.md +++ b/docs/examples/apps/get-installation.md @@ -6,7 +6,7 @@ from appwrite.models import AppInstallation 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 +client.set_session('') # The user session to authenticate with apps = Apps(client) diff --git a/docs/examples/apps/list-installations.md b/docs/examples/apps/list-installations.md index f5e292b2..e21575cb 100644 --- a/docs/examples/apps/list-installations.md +++ b/docs/examples/apps/list-installations.md @@ -6,7 +6,7 @@ from appwrite.models import AppInstallationList 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 +client.set_session('') # The user session to authenticate with apps = Apps(client) diff --git a/docs/examples/embeddings/create-text-embeddings.md b/docs/examples/embeddings/create-text-embeddings.md new file mode 100644 index 00000000..49d3c258 --- /dev/null +++ b/docs/examples/embeddings/create-text-embeddings.md @@ -0,0 +1,20 @@ +```python +from appwrite.client import Client +from appwrite.services.embeddings import Embeddings +from appwrite.models import EmbeddingList +from appwrite.enums import EmbeddingModel + +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 + +embeddings = Embeddings(client) + +result: EmbeddingList = embeddings.create_text_embeddings( + texts = [], + model = EmbeddingModel.NOMIC_EMBED_TEXT # optional +) + +print(result.model_dump()) +``` diff --git a/docs/examples/project/get-policy.md b/docs/examples/project/get-policy.md index f9487669..16caef14 100644 --- a/docs/examples/project/get-policy.md +++ b/docs/examples/project/get-policy.md @@ -11,6 +11,7 @@ from appwrite.models import PolicySessionInvalidation from appwrite.models import PolicySessionLimit from appwrite.models import PolicyUserLimit from appwrite.models import PolicyMembershipPrivacy +from appwrite.models import PolicyMfaFactors from appwrite.models import PolicyDenyAliasedEmail from appwrite.models import PolicyDenyDisposableEmail from appwrite.models import PolicyDenyFreeEmail @@ -25,7 +26,7 @@ client.set_key('') # Your secret API key project = Project(client) -result: Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail] = project.get_policy( +result: Union[PolicyPasswordDictionary, PolicyPasswordHistory, PolicyPasswordStrength, PolicyPasswordPersonalData, PolicySessionAlert, PolicySessionDuration, PolicySessionInvalidation, PolicySessionLimit, PolicyUserLimit, PolicyMembershipPrivacy, PolicyMfaFactors, PolicyDenyAliasedEmail, PolicyDenyDisposableEmail, PolicyDenyFreeEmail, PolicyDenyCorporateEmail] = project.get_policy( policy_id = ProjectPolicyId.PASSWORD_DICTIONARY ) diff --git a/docs/examples/project/create-key.md b/docs/examples/project/update-mfa-factors-policy.md similarity index 58% rename from docs/examples/project/create-key.md rename to docs/examples/project/update-mfa-factors-policy.md index a3e040f7..67f75688 100644 --- a/docs/examples/project/create-key.md +++ b/docs/examples/project/update-mfa-factors-policy.md @@ -1,8 +1,7 @@ ```python from appwrite.client import Client from appwrite.services.project import Project -from appwrite.models import Key -from appwrite.enums import ProjectKeyScopes +from appwrite.models import Project as ProjectModel client = Client() client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint @@ -11,11 +10,11 @@ client.set_key('') # Your secret API key project = Project(client) -result: Key = project.create_key( - key_id = '', - name = '', - scopes = [ProjectKeyScopes.PROJECT_READ], - expire = '2020-10-15T06:38:00.000+00:00' # optional +result: ProjectModel = project.update_mfa_factors_policy( + totp = False, # optional + email = False, # optional + phone = False, # optional + custom = False # optional ) print(result.model_dump()) diff --git a/docs/examples/project/update-o-auth-2-server.md b/docs/examples/project/update-o-auth-2-server.md index bce0876e..338ebf33 100644 --- a/docs/examples/project/update-o-auth-2-server.md +++ b/docs/examples/project/update-o-auth-2-server.md @@ -25,7 +25,8 @@ result: ProjectModel = project.update_o_auth2_server( user_code_length = 6, # optional user_code_format = 'numeric', # optional device_code_duration = 60, # optional - default_scopes = [] # optional + default_scopes = [], # optional + installation_scopes = [] # optional ) print(result.model_dump()) diff --git a/docs/examples/project/update-session-duration-policy.md b/docs/examples/project/update-session-duration-policy.md index 768bd4ff..817947e4 100644 --- a/docs/examples/project/update-session-duration-policy.md +++ b/docs/examples/project/update-session-duration-policy.md @@ -11,7 +11,7 @@ client.set_key('') # Your secret API key project = Project(client) result: ProjectModel = project.update_session_duration_policy( - duration = 5 + duration = 60 ) print(result.model_dump()) diff --git a/docs/examples/project/update-user-limit-policy.md b/docs/examples/project/update-user-limit-policy.md index e841acae..42d66de3 100644 --- a/docs/examples/project/update-user-limit-policy.md +++ b/docs/examples/project/update-user-limit-policy.md @@ -11,7 +11,7 @@ client.set_key('') # Your secret API key project = Project(client) result: ProjectModel = project.update_user_limit_policy( - total = 1 + total = 0 ) print(result.model_dump()) diff --git a/docs/examples/proxy/create-invalidation.md b/docs/examples/proxy/create-invalidation.md new file mode 100644 index 00000000..f94b4168 --- /dev/null +++ b/docs/examples/proxy/create-invalidation.md @@ -0,0 +1,21 @@ +```python +from appwrite.client import Client +from appwrite.services.proxy import Proxy +from appwrite.models import ProxyInvalidation +from appwrite.enums import InvalidationType + +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: ProxyInvalidation = proxy.create_invalidation( + domain = '', + type = InvalidationType.TAG, + reference = '' # optional +) + +print(result.model_dump()) +``` diff --git a/docs/examples/storage/create-file.md b/docs/examples/storage/create-file.md index 2103a1d6..cfec75c1 100644 --- a/docs/examples/storage/create-file.md +++ b/docs/examples/storage/create-file.md @@ -17,7 +17,8 @@ result: File = storage.create_file( bucket_id = '', file_id = '', file = InputFile.from_path('file.png'), - permissions = [Permission.read(Role.any())] # optional + permissions = [Permission.read(Role.any())], # optional + folder = '' # optional ) print(result.model_dump()) diff --git a/docs/examples/tablesdb/create-migration.md b/docs/examples/tablesdb/create-migration.md new file mode 100644 index 00000000..46dd29bc --- /dev/null +++ b/docs/examples/tablesdb/create-migration.md @@ -0,0 +1,20 @@ +```python +from appwrite.client import Client +from appwrite.services.tables_db import TablesDB +from appwrite.models import DatabaseMigration + +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 + +tables_db = TablesDB(client) + +result: DatabaseMigration = tables_db.create_migration( + database_id = '', + specification = 's-1vcpu-1gb', + auto_cutover = False # optional +) + +print(result.model_dump()) +``` diff --git a/docs/examples/tablesdb/create.md b/docs/examples/tablesdb/create.md index 0c5260cf..ca668875 100644 --- a/docs/examples/tablesdb/create.md +++ b/docs/examples/tablesdb/create.md @@ -15,7 +15,8 @@ result: Database = tables_db.create( name = '', enabled = False, # optional specification = 'serverless', # optional - replicas = 0 # optional + replicas = 0, # optional + sync_mode = 'async' # optional ) print(result.model_dump()) diff --git a/docs/examples/tablesdb/cutover-migration.md b/docs/examples/tablesdb/cutover-migration.md new file mode 100644 index 00000000..1b793d63 --- /dev/null +++ b/docs/examples/tablesdb/cutover-migration.md @@ -0,0 +1,19 @@ +```python +from appwrite.client import Client +from appwrite.services.tables_db import TablesDB +from appwrite.models import DatabaseMigration + +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 + +tables_db = TablesDB(client) + +result: DatabaseMigration = tables_db.cutover_migration( + database_id = '', + migration_id = '' +) + +print(result.model_dump()) +``` diff --git a/docs/examples/tablesdb/delete-migration.md b/docs/examples/tablesdb/delete-migration.md new file mode 100644 index 00000000..9442ae34 --- /dev/null +++ b/docs/examples/tablesdb/delete-migration.md @@ -0,0 +1,16 @@ +```python +from appwrite.client import Client +from appwrite.services.tables_db import TablesDB + +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 + +tables_db = TablesDB(client) + +result = tables_db.delete_migration( + database_id = '', + migration_id = '' +) +``` diff --git a/docs/examples/tablesdb/get-migration.md b/docs/examples/tablesdb/get-migration.md new file mode 100644 index 00000000..2efe0b63 --- /dev/null +++ b/docs/examples/tablesdb/get-migration.md @@ -0,0 +1,19 @@ +```python +from appwrite.client import Client +from appwrite.services.tables_db import TablesDB +from appwrite.models import DatabaseMigration + +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 + +tables_db = TablesDB(client) + +result: DatabaseMigration = tables_db.get_migration( + database_id = '', + migration_id = '' +) + +print(result.model_dump()) +``` diff --git a/docs/examples/tablesdb/list-migrations.md b/docs/examples/tablesdb/list-migrations.md new file mode 100644 index 00000000..99665efa --- /dev/null +++ b/docs/examples/tablesdb/list-migrations.md @@ -0,0 +1,18 @@ +```python +from appwrite.client import Client +from appwrite.services.tables_db import TablesDB +from appwrite.models import DatabaseMigrationList + +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 + +tables_db = TablesDB(client) + +result: DatabaseMigrationList = tables_db.list_migrations( + database_id = '' +) + +print(result.model_dump()) +``` diff --git a/docs/examples/tablesdb/list-operations.md b/docs/examples/tablesdb/list-operations.md new file mode 100644 index 00000000..afe14338 --- /dev/null +++ b/docs/examples/tablesdb/list-operations.md @@ -0,0 +1,21 @@ +```python +from appwrite.client import Client +from appwrite.services.tables_db import TablesDB +from appwrite.models import DedicatedDatabaseOperationList + +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 + +tables_db = TablesDB(client) + +result: DedicatedDatabaseOperationList = tables_db.list_operations( + database_id = '', + status = 'running', # optional + limit = 1, # optional + offset = 0 # optional +) + +print(result.model_dump()) +``` diff --git a/docs/examples/tablesdb/update.md b/docs/examples/tablesdb/update.md index 8609072e..7fc0c930 100644 --- a/docs/examples/tablesdb/update.md +++ b/docs/examples/tablesdb/update.md @@ -14,7 +14,9 @@ result: Database = tables_db.update( database_id = '', name = '', # optional enabled = False, # optional - replicas = 0 # optional + specification = 'serverless', # optional + replicas = 0, # optional + sync_mode = 'async' # optional ) print(result.model_dump()) diff --git a/docs/examples/users/get-mfa-challenge.md b/docs/examples/users/get-mfa-challenge.md new file mode 100644 index 00000000..8dde75a7 --- /dev/null +++ b/docs/examples/users/get-mfa-challenge.md @@ -0,0 +1,19 @@ +```python +from appwrite.client import Client +from appwrite.services.users import Users +from appwrite.models import MfaChallengeSecret + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_key('') # Your secret API key + +users = Users(client) + +result: MfaChallengeSecret = users.get_mfa_challenge( + user_id = '', + challenge_id = '' +) + +print(result.model_dump()) +``` diff --git a/pyproject.toml b/pyproject.toml index b71d5337..f01bffcd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "appwrite" -version = "22.2.0" +version = "23.0.0" description = "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API" readme = "README.md" requires-python = ">=3.9" diff --git a/setup.py b/setup.py index 3d51ebff..980aaf26 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setuptools.setup( name = 'appwrite', packages = setuptools.find_packages(), - version = '22.2.0', + version = '23.0.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/22.2.0.tar.gz', + download_url='https://github.com/appwrite/sdk-for-python/archive/23.0.0.tar.gz', install_requires=[ 'requests', 'pydantic>=2,<3', diff --git a/test/services/test_account.py b/test/services/test_account.py index c0bab652..648aba16 100644 --- a/test/services/test_account.py +++ b/test/services/test_account.py @@ -230,19 +230,6 @@ def test_delete_identity(self, m): self.assertEqual(response, data) - @requests_mock.Mocker() - def test_create_jwt(self, m): - data = { - "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" -} - headers = {'Content-Type': 'application/json'} - m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) - - response = self.account.create_jwt( - ) - - self.assertEqual(response.to_dict(), data) - @requests_mock.Mocker() def test_list_logs(self, m): data = { @@ -526,7 +513,8 @@ def test_list_mfa_factors(self, m): "totp": True, "phone": True, "email": True, - "recoveryCode": True + "recoveryCode": True, + "custom": True } headers = {'Content-Type': 'application/json'} m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) @@ -542,7 +530,8 @@ def test_list_mfa_factors(self, m): "totp": True, "phone": True, "email": True, - "recoveryCode": True + "recoveryCode": True, + "custom": True } headers = {'Content-Type': 'application/json'} m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) @@ -727,7 +716,6 @@ def test_get_prefs(self, m): response = self.account.get_prefs( ) - data['data'] = {} self.assertEqual(response.to_dict(), data) @requests_mock.Mocker() diff --git a/test/services/test_apps.py b/test/services/test_apps.py index e923f2e5..8f9145a3 100644 --- a/test/services/test_apps.py +++ b/test/services/test_apps.py @@ -225,6 +225,19 @@ def test_get_installation(self, m): self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() + def test_delete_installation(self, m): + data = '' + headers = {'Content-Type': 'application/json'} + m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) + + response = self.apps.delete_installation( + '', + '', + ) + + self.assertEqual(response, data) + @requests_mock.Mocker() def test_create_installation_token(self, m): data = { diff --git a/test/services/test_embeddings.py b/test/services/test_embeddings.py new file mode 100644 index 00000000..8d2cf848 --- /dev/null +++ b/test/services/test_embeddings.py @@ -0,0 +1,30 @@ +import json +import requests_mock +import unittest + +from appwrite.client import Client +from appwrite.input_file import InputFile +from appwrite.models import * +from appwrite.services.embeddings import Embeddings + +class EmbeddingsServiceTest(unittest.TestCase): + + def setUp(self): + self.client = Client() + self.embeddings = Embeddings(self.client) + + @requests_mock.Mocker() + def test_create_text_embeddings(self, m): + data = { + "total": 5.0, + "embeddings": [] +} + headers = {'Content-Type': 'application/json'} + m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) + + response = self.embeddings.create_text_embeddings( + [], + ) + + self.assertEqual(response.to_dict(), data) + diff --git a/test/services/test_organization.py b/test/services/test_organization.py index 544d647d..2bf4dbc8 100644 --- a/test/services/test_organization.py +++ b/test/services/test_organization.py @@ -22,7 +22,6 @@ def test_get(self, m): "name": "VIP", "total": 7.0, "prefs": {}, - "billingBudget": 50.0, "budgetAlerts": [], "billingPlan": "tier-1", "billingPlanId": "tier-1", @@ -37,7 +36,6 @@ def test_get(self, m): "storage": 25.0, "imageTransformations": 100.0, "screenshotsGenerated": 50.0, - "members": 25.0, "webhooks": 25.0, "wafRules": 2.0, "projects": 2.0, @@ -61,7 +59,6 @@ def test_get(self, m): "topics": 1.0, "authPhone": 10.0, "domains": 5.0, - "activityLogs": 7.0, "usageLogs": 30.0, "projectInactivityDays": 7.0, "alertLimit": 80.0, @@ -82,14 +79,6 @@ def test_get(self, m): "value": 25.0, "invoiceDesc": "" }, - "member": { - "name": "", - "unit": "GB", - "currency": "USD", - "price": 5, - "value": 25.0, - "invoiceDesc": "" - }, "realtime": { "name": "", "unit": "GB", @@ -106,14 +95,6 @@ def test_get(self, m): "value": 25.0, "invoiceDesc": "" }, - "realtimeBandwidth": { - "name": "", - "unit": "GB", - "currency": "USD", - "price": 5, - "value": 25.0, - "invoiceDesc": "" - }, "storage": { "name": "", "unit": "GB", @@ -145,38 +126,9 @@ def test_get(self, m): "price": 5, "value": 25.0, "invoiceDesc": "" - }, - "credits": { - "name": "", - "unit": "GB", - "currency": "USD", - "price": 5, - "value": 25.0, - "invoiceDesc": "" - } - }, - "addons": { - "seats": { - "supported": True, - "planIncluded": 1.0, - "limit": 5.0, - "type": "numeric", - "currency": "USD", - "price": 5, - "value": 25.0, - "invoiceDesc": "" - }, - "projects": { - "supported": True, - "planIncluded": 1.0, - "limit": 5.0, - "type": "numeric", - "currency": "USD", - "price": 5, - "value": 25.0, - "invoiceDesc": "" } }, + "addons": {}, "budgetCapEnabled": True, "customSmtp": True, "emailBranding": True, @@ -194,14 +146,12 @@ def test_get(self, m): "supportsFreeEmailValidation": True, "supportsCorporateEmailValidation": True, "supportsProjectSpecificRoles": True, - "backupsEnabled": True, "usagePerProject": True, "supportedAddons": { "baa": True, "premiumGeoDB": True, "premiumGeoDBOrg": True }, - "backupPolicies": 1.0, "deploymentSize": 30.0, "buildSize": 2000.0, "databasesAllowEncrypt": True, @@ -211,22 +161,11 @@ def test_get(self, m): "billingStartDate": "2020-10-15T06:38:00.000+00:00", "billingCurrentInvoiceDate": "2020-10-15T06:38:00.000+00:00", "billingNextInvoiceDate": "2020-10-15T06:38:00.000+00:00", - "billingTrialStartDate": "2020-10-15T06:38:00.000+00:00", "billingTrialDays": 14.0, "billingAggregationId": "adbc3de4rddfsd", "billingInvoiceId": "adbc3de4rddfsd", "paymentMethodId": "adbc3de4rddfsd", - "billingAddressId": "adbc3de4rddfsd", - "backupPaymentMethodId": "adbc3de4rddfsd", "status": "active", - "remarks": "Pending initial payment", - "agreementBAA": "", - "programManagerName": "", - "programManagerCalendar": "", - "programDiscordChannelName": "", - "programDiscordChannelUrl": "", - "billingPlanDowngrade": "tier-1", - "billingTaxId": "", "markedForDeletion": True, "platform": "imagine", "projects": [] @@ -248,7 +187,6 @@ def test_update(self, m): "name": "VIP", "total": 7.0, "prefs": {}, - "billingBudget": 50.0, "budgetAlerts": [], "billingPlan": "tier-1", "billingPlanId": "tier-1", @@ -263,7 +201,6 @@ def test_update(self, m): "storage": 25.0, "imageTransformations": 100.0, "screenshotsGenerated": 50.0, - "members": 25.0, "webhooks": 25.0, "wafRules": 2.0, "projects": 2.0, @@ -287,7 +224,6 @@ def test_update(self, m): "topics": 1.0, "authPhone": 10.0, "domains": 5.0, - "activityLogs": 7.0, "usageLogs": 30.0, "projectInactivityDays": 7.0, "alertLimit": 80.0, @@ -308,14 +244,6 @@ def test_update(self, m): "value": 25.0, "invoiceDesc": "" }, - "member": { - "name": "", - "unit": "GB", - "currency": "USD", - "price": 5, - "value": 25.0, - "invoiceDesc": "" - }, "realtime": { "name": "", "unit": "GB", @@ -332,14 +260,6 @@ def test_update(self, m): "value": 25.0, "invoiceDesc": "" }, - "realtimeBandwidth": { - "name": "", - "unit": "GB", - "currency": "USD", - "price": 5, - "value": 25.0, - "invoiceDesc": "" - }, "storage": { "name": "", "unit": "GB", @@ -371,38 +291,9 @@ def test_update(self, m): "price": 5, "value": 25.0, "invoiceDesc": "" - }, - "credits": { - "name": "", - "unit": "GB", - "currency": "USD", - "price": 5, - "value": 25.0, - "invoiceDesc": "" - } - }, - "addons": { - "seats": { - "supported": True, - "planIncluded": 1.0, - "limit": 5.0, - "type": "numeric", - "currency": "USD", - "price": 5, - "value": 25.0, - "invoiceDesc": "" - }, - "projects": { - "supported": True, - "planIncluded": 1.0, - "limit": 5.0, - "type": "numeric", - "currency": "USD", - "price": 5, - "value": 25.0, - "invoiceDesc": "" } }, + "addons": {}, "budgetCapEnabled": True, "customSmtp": True, "emailBranding": True, @@ -420,14 +311,12 @@ def test_update(self, m): "supportsFreeEmailValidation": True, "supportsCorporateEmailValidation": True, "supportsProjectSpecificRoles": True, - "backupsEnabled": True, "usagePerProject": True, "supportedAddons": { "baa": True, "premiumGeoDB": True, "premiumGeoDBOrg": True }, - "backupPolicies": 1.0, "deploymentSize": 30.0, "buildSize": 2000.0, "databasesAllowEncrypt": True, @@ -437,22 +326,11 @@ def test_update(self, m): "billingStartDate": "2020-10-15T06:38:00.000+00:00", "billingCurrentInvoiceDate": "2020-10-15T06:38:00.000+00:00", "billingNextInvoiceDate": "2020-10-15T06:38:00.000+00:00", - "billingTrialStartDate": "2020-10-15T06:38:00.000+00:00", "billingTrialDays": 14.0, "billingAggregationId": "adbc3de4rddfsd", "billingInvoiceId": "adbc3de4rddfsd", "paymentMethodId": "adbc3de4rddfsd", - "billingAddressId": "adbc3de4rddfsd", - "backupPaymentMethodId": "adbc3de4rddfsd", "status": "active", - "remarks": "Pending initial payment", - "agreementBAA": "", - "programManagerName": "", - "programManagerCalendar": "", - "programDiscordChannelName": "", - "programDiscordChannelUrl": "", - "billingPlanDowngrade": "tier-1", - "billingTaxId": "", "markedForDeletion": True, "platform": "imagine", "projects": [] diff --git a/test/services/test_project.py b/test/services/test_project.py index b4b9c640..9f8f36e0 100644 --- a/test/services/test_project.py +++ b/test/services/test_project.py @@ -120,30 +120,6 @@ def test_list_keys(self, m): self.assertEqual(response.to_dict(), data) - @requests_mock.Mocker() - def test_create_key(self, m): - data = { - "$id": "5e5ea5c16897e", - "$createdAt": "2020-10-15T06:38:00.000+00:00", - "$updatedAt": "2020-10-15T06:38:00.000+00:00", - "name": "My API Key", - "expire": "2020-10-15T06:38:00.000+00:00", - "scopes": [], - "secret": "919c2d18fb5d4...a2ae413da83346ad2", - "accessedAt": "2020-10-15T06:38:00.000+00:00", - "sdks": [] -} - headers = {'Content-Type': 'application/json'} - m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) - - response = self.project.create_key( - '', - '', - [], - ) - - self.assertEqual(response.to_dict(), data) - @requests_mock.Mocker() def test_create_ephemeral_key(self, m): data = { @@ -1596,6 +1572,46 @@ def test_update_membership_privacy_policy(self, m): self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() + def test_update_mfa_factors_policy(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "name": "New Project", + "teamId": "1592981250", + "region": "fra", + "devKeys": [], + "smtpEnabled": True, + "smtpSenderName": "John Appwrite", + "smtpSenderEmail": "john@appwrite.io", + "smtpReplyToName": "Support Team", + "smtpReplyToEmail": "support@appwrite.io", + "smtpHost": "mail.appwrite.io", + "smtpPort": 25.0, + "smtpUsername": "emailuser", + "smtpPassword": "smtp-password", + "smtpSecure": "tls", + "pingCount": 1.0, + "pingedAt": "2020-10-15T06:38:00.000+00:00", + "labels": [], + "status": "active", + "onboarding": {}, + "authMethods": [], + "services": [], + "protocols": [], + "blocks": [], + "consoleAccessedAt": "2020-10-15T06:38:00.000+00:00", + "wafEnabled": True +} + headers = {'Content-Type': 'application/json'} + m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) + + response = self.project.update_mfa_factors_policy( + ) + + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_update_password_dictionary_policy(self, m): data = { diff --git a/test/services/test_proxy.py b/test/services/test_proxy.py index 5da8c18c..0891cc85 100644 --- a/test/services/test_proxy.py +++ b/test/services/test_proxy.py @@ -13,6 +13,24 @@ def setUp(self): self.client = Client() self.proxy = Proxy(self.client) + @requests_mock.Mocker() + def test_create_invalidation(self, m): + data = { + "domain": "appwrite.company.com", + "type": "tag", + "reference": "products", + "status": "success" +} + headers = {'Content-Type': 'application/json'} + m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) + + response = self.proxy.create_invalidation( + '', + 'tag', + ) + + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_list_rules(self, m): data = { diff --git a/test/services/test_storage.py b/test/services/test_storage.py index 08ca786e..af4587cb 100644 --- a/test/services/test_storage.py +++ b/test/services/test_storage.py @@ -146,6 +146,8 @@ def test_create_file(self, m): "$updatedAt": "2020-10-15T06:38:00.000+00:00", "$permissions": [], "name": "Pink.png", + "folder": "photos\/2026\/", + "key": "photos\/2026\/Pink.png", "signature": "5d529fd02b544198ae075bd57c1762bb", "mimeType": "image\/png", "sizeOriginal": 17890.0, @@ -175,6 +177,8 @@ def test_get_file(self, m): "$updatedAt": "2020-10-15T06:38:00.000+00:00", "$permissions": [], "name": "Pink.png", + "folder": "photos\/2026\/", + "key": "photos\/2026\/Pink.png", "signature": "5d529fd02b544198ae075bd57c1762bb", "mimeType": "image\/png", "sizeOriginal": 17890.0, @@ -203,6 +207,8 @@ def test_update_file(self, m): "$updatedAt": "2020-10-15T06:38:00.000+00:00", "$permissions": [], "name": "Pink.png", + "folder": "photos\/2026\/", + "key": "photos\/2026\/Pink.png", "signature": "5d529fd02b544198ae075bd57c1762bb", "mimeType": "image\/png", "sizeOriginal": 17890.0, diff --git a/test/services/test_tables_db.py b/test/services/test_tables_db.py index 888be71f..ba339712 100644 --- a/test/services/test_tables_db.py +++ b/test/services/test_tables_db.py @@ -56,7 +56,6 @@ def test_list_specifications(self, m): "storageOverageRate": 0.125, "bandwidthOverageRate": 0.08, "replicaRate": 1, - "crossRegionReplicaRate": 1, "pitrRate": 0.2 } } @@ -250,7 +249,6 @@ def test_create_failover(self, m): "nodePool": "db-pool-4vcpu-8gb", "replicas": 2.0, "syncMode": "async", - "crossRegionReplicas": 1.0, "networkMaxConnections": 500.0, "networkIdleTimeoutSeconds": 900.0, "networkIPAllowlist": [], @@ -279,11 +277,147 @@ def test_create_failover(self, m): self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() + def test_list_migrations(self, m): + data = { + "total": 5.0, + "migrations": [] +} + headers = {'Content-Type': 'application/json'} + m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) + + response = self.tables_db.list_migrations( + '', + ) + + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_create_migration(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "projectId": "5e5ea5c16897e", + "databaseId": "5e5ea5c16897e", + "specification": "s-2vcpu-4gb", + "phase": "pending", + "attempt": 0.0, + "lastError": "", + "lagDocuments": 0.0, + "verifiedAt": "2020-10-15T06:38:00.000+00:00", + "cutoverAt": "2020-10-15T06:38:00.000+00:00", + "soakUntil": "2020-10-15T06:38:00.000+00:00", + "autoCutover": True, + "cutoverRequested": True, + "paused": True +} + headers = {'Content-Type': 'application/json'} + m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) + + response = self.tables_db.create_migration( + '', + 's-1vcpu-1gb', + ) + + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_get_migration(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "projectId": "5e5ea5c16897e", + "databaseId": "5e5ea5c16897e", + "specification": "s-2vcpu-4gb", + "phase": "pending", + "attempt": 0.0, + "lastError": "", + "lagDocuments": 0.0, + "verifiedAt": "2020-10-15T06:38:00.000+00:00", + "cutoverAt": "2020-10-15T06:38:00.000+00:00", + "soakUntil": "2020-10-15T06:38:00.000+00:00", + "autoCutover": True, + "cutoverRequested": True, + "paused": True +} + headers = {'Content-Type': 'application/json'} + m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) + + response = self.tables_db.get_migration( + '', + '', + ) + + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_delete_migration(self, m): + data = '' + headers = {'Content-Type': 'application/json'} + m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) + + response = self.tables_db.delete_migration( + '', + '', + ) + + self.assertEqual(response, data) + + @requests_mock.Mocker() + def test_cutover_migration(self, m): + data = { + "$id": "5e5ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "projectId": "5e5ea5c16897e", + "databaseId": "5e5ea5c16897e", + "specification": "s-2vcpu-4gb", + "phase": "pending", + "attempt": 0.0, + "lastError": "", + "lagDocuments": 0.0, + "verifiedAt": "2020-10-15T06:38:00.000+00:00", + "cutoverAt": "2020-10-15T06:38:00.000+00:00", + "soakUntil": "2020-10-15T06:38:00.000+00:00", + "autoCutover": True, + "cutoverRequested": True, + "paused": True +} + headers = {'Content-Type': 'application/json'} + m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) + + response = self.tables_db.cutover_migration( + '', + '', + ) + + self.assertEqual(response.to_dict(), data) + + @requests_mock.Mocker() + def test_list_operations(self, m): + data = { + "total": 5.0, + "operations": [] +} + headers = {'Content-Type': 'application/json'} + m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) + + response = self.tables_db.list_operations( + '', + ) + + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_get_replicas(self, m): data = { "replicas": 2.0, "syncMode": "async", + "syncDegraded": True, + "syncAcknowledgements": 1.0, + "syncStandbyCount": 2.0, "members": [] } headers = {'Content-Type': 'application/json'} @@ -307,6 +441,10 @@ def test_get_status(self, m): "current": 12.0, "max": 100.0 }, + "syncMode": "async", + "syncDegraded": True, + "syncAcknowledgements": 1.0, + "syncStandbyCount": 2.0, "replicas": [], "volumes": [] } diff --git a/test/services/test_teams.py b/test/services/test_teams.py index 684d00f8..9c2635f5 100644 --- a/test/services/test_teams.py +++ b/test/services/test_teams.py @@ -352,7 +352,6 @@ def test_get_prefs(self, m): '', ) - data['data'] = {} self.assertEqual(response.to_dict(), data) @requests_mock.Mocker() @@ -366,6 +365,5 @@ def test_update_prefs(self, m): {}, ) - data['data'] = {} self.assertEqual(response.to_dict(), data) diff --git a/test/services/test_users.py b/test/services/test_users.py index cd4eb71f..efb2130d 100644 --- a/test/services/test_users.py +++ b/test/services/test_users.py @@ -568,13 +568,33 @@ def test_delete_mfa_authenticator(self, m): self.assertEqual(response, data) + @requests_mock.Mocker() + def test_get_mfa_challenge(self, m): + data = { + "$id": "bb8ea5c16897e", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "userId": "5e5ea5c168bb8", + "expire": "2020-10-15T06:38:00.000+00:00", + "code": "446372" +} + headers = {'Content-Type': 'application/json'} + m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) + + response = self.users.get_mfa_challenge( + '', + '', + ) + + self.assertEqual(response.to_dict(), data) + @requests_mock.Mocker() def test_list_mfa_factors(self, m): data = { "totp": True, "phone": True, "email": True, - "recoveryCode": True + "recoveryCode": True, + "custom": True } headers = {'Content-Type': 'application/json'} m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) @@ -591,7 +611,8 @@ def test_list_mfa_factors(self, m): "totp": True, "phone": True, "email": True, - "recoveryCode": True + "recoveryCode": True, + "custom": True } headers = {'Content-Type': 'application/json'} m.request(requests_mock.ANY, requests_mock.ANY, text=json.dumps(data), headers=headers) @@ -786,7 +807,6 @@ def test_get_prefs(self, m): '', ) - data['data'] = {} self.assertEqual(response.to_dict(), data) @requests_mock.Mocker() @@ -800,7 +820,6 @@ def test_update_prefs(self, m): {}, ) - data['data'] = {} self.assertEqual(response.to_dict(), data) @requests_mock.Mocker()