diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml
new file mode 100644
index 0000000000..ea2676cd4a
--- /dev/null
+++ b/.github/workflows/publish-python.yml
@@ -0,0 +1,107 @@
+name: Publish Python package
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ type: string
+ required: true
+ description: >
+ PEP 440 version to publish (e.g. 0.1.0, 1.0.0a1, 1.0.0rc1). See
+ https://packaging.python.org/en/latest/specifications/version-specifiers/
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: write
+ id-token: write
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ token: ${{ secrets.RELEASE_COMMIT_WRITE_TOKEN }}
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
+
+ - name: Validate version
+ env:
+ VERSION: ${{ inputs.version }}
+ run: |
+ uv run --with packaging --no-project -- python - <<'PY'
+ import os
+ import sys
+ from packaging.version import Version, InvalidVersion
+
+ version = os.environ["VERSION"]
+
+ try:
+ parsed = Version(version)
+ except InvalidVersion:
+ print(f"::error::'{version}' is not a valid PEP 440 version. "
+ "Examples: 0.1.0, 1.0.0a1, 1.0.0b2, 1.0.0rc1, 1.0.0.post1, 1.0.0.dev1")
+ raise SystemExit(1)
+
+ if str(parsed) != version:
+ print(f"::error::Version must be in canonical form. "
+ f"Got '{version}', expected '{parsed}'.")
+ raise SystemExit(1)
+
+ if len(parsed.release) != 3:
+ print("::error::Version must use exactly three release components: X.Y.Z "
+ "(examples: 0.1.0, 1.0.0rc1, 1.0.0.post1, 1.0.0.dev1).")
+ raise SystemExit(1)
+
+ if parsed.epoch != 0:
+ print("::error::Epochs are not allowed for releases.")
+ raise SystemExit(1)
+
+ if parsed.local is not None:
+ print("::error::Local versions (+...) are not allowed for releases.")
+ raise SystemExit(1)
+
+ print(f"Validated version: {version}")
+ PY
+
+ - name: Setup git config
+ run: |
+ git config user.name "GitHub Actions Bot"
+ git config user.email "<>"
+
+ - name: Set version in pyproject.toml
+ working-directory: packages/liveblocks-python
+ env:
+ VERSION: ${{ inputs.version }}
+ run: |
+ sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
+
+ - name: Set version in codegen config
+ working-directory: packages/liveblocks-python-codegen
+ env:
+ VERSION: ${{ inputs.version }}
+ run: |
+ sed -i "s/^package_version_override: .*/package_version_override: $VERSION/" config.yaml
+
+ - name: Commit and push version bump
+ env:
+ VERSION: ${{ inputs.version }}
+ run: |
+ git add packages/liveblocks-python/pyproject.toml packages/liveblocks-python-codegen/config.yaml
+ git commit -m "Bump liveblocks-python to $VERSION"
+ git push origin HEAD
+
+ - name: Build and publish
+ working-directory: packages/liveblocks-python
+ run: |
+ uv build
+ uv publish --trusted-publishing always
+
+ - name: Create release tag
+ env:
+ VERSION: ${{ inputs.version }}
+ GIT_TAG: python-v${{ inputs.version }}
+ run: |
+ git tag "$GIT_TAG" -m "Release liveblocks-python $VERSION"
+ git push origin "$GIT_TAG"
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5f1d605a30..ed94cc2c84 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -304,3 +304,94 @@ jobs:
if: needs.check_for_code_changes.outputs.changes == 'true'
run: npm run test:e2e
working-directory: packages/liveblocks-core
+
+ # Check that the generated Python SDK is up to date with the OpenAPI spec
+ python-sdk-sync:
+ runs-on: ubuntu-latest
+
+ permissions:
+ pull-requests: read
+
+ steps:
+ - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
+ id: filter
+ with:
+ filters: |
+ python_sdk:
+ - 'docs/references/v2.openapi.json'
+ - 'packages/liveblocks-python-codegen/templates/**'
+ - 'packages/liveblocks-python-codegen/config.yaml'
+ - 'packages/liveblocks-python-codegen/generate.py'
+
+ - name: Checkout
+ if: steps.filter.outputs.python_sdk == 'true'
+ uses: actions/checkout@v4
+
+ - name: Install uv
+ if: steps.filter.outputs.python_sdk == 'true'
+ uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
+
+ - name: Regenerate Python SDK
+ if: steps.filter.outputs.python_sdk == 'true'
+ run: uv run python generate.py
+ working-directory: packages/liveblocks-python-codegen
+
+ - name: Check for uncommitted changes
+ if: steps.filter.outputs.python_sdk == 'true'
+ run: |
+ CHANGED=$(git status --porcelain packages/liveblocks-python)
+ if [ -n "$CHANGED" ]; then
+ echo "The generated Python SDK is out of date. Please run 'uv run python generate.py' in packages/liveblocks-python-codegen and commit the changes."
+ echo ""
+ echo "Changed files:"
+ echo "$CHANGED"
+ echo ""
+ git diff packages/liveblocks-python
+ exit 1
+ fi
+
+ # Python lint + tests with coverage
+ test-python:
+ runs-on: ubuntu-latest
+
+ permissions:
+ pull-requests: write
+
+ steps:
+ - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
+ id: filter
+ with:
+ filters: |
+ python_sdk:
+ - 'packages/liveblocks-python/**'
+ - 'packages/liveblocks-python-codegen/**'
+
+ - name: Checkout
+ if: steps.filter.outputs.python_sdk == 'true'
+ uses: actions/checkout@v4
+
+ - name: Install uv
+ if: steps.filter.outputs.python_sdk == 'true'
+ uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
+
+ - name: Lint
+ if: steps.filter.outputs.python_sdk == 'true'
+ run: uvx ruff check .
+ working-directory: packages/liveblocks-python
+
+ - name: Run Python tests with coverage
+ if: steps.filter.outputs.python_sdk == 'true'
+ run:
+ uv run pytest --cov --cov-report=term-missing
+ --cov-report=xml:coverage.xml
+ working-directory: packages/liveblocks-python
+
+ - name: Post coverage report as PR comment
+ if:
+ steps.filter.outputs.python_sdk == 'true' && github.event_name ==
+ 'pull_request'
+ uses: orgoro/coverage@3f13a558c5af7376496aa4848bf0224aead366ac # v3.2
+ with:
+ coverageFile: packages/liveblocks-python/coverage.xml
+ token: ${{ secrets.GITHUB_TOKEN }}
+ thresholdAll: 0.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab60d9dadb..ad0ab1b65d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
## vNEXT (not yet released)
+## v3.15.3
+
+### `@liveblocks/react-ui`
+
+- Add `showSubscription` prop to `Thread` to control whether to show the
+ thread’s subscription status.
+
## v3.15.2
### `@liveblocks/client`
diff --git a/docs/pages/api-reference/liveblocks-react-ui.mdx b/docs/pages/api-reference/liveblocks-react-ui.mdx
index f92925c278..53870f99ef 100644
--- a/docs/pages/api-reference/liveblocks-react-ui.mdx
+++ b/docs/pages/api-reference/liveblocks-react-ui.mdx
@@ -1195,6 +1195,13 @@ icon next to the item’s label.
>
Whether to show deleted comments.
+
+ Whether to show the thread’s subscription status.
+
_Excluding features related to Comments and Notifications._
-² _Basic room APIs are partially supported, but room permissions and
-metadata are not fully implemented yet._
-³ _Basic APIs will return dummy data, so you can still use the Liveblocks
+² _Basic APIs will return dummy data, so you can still use the Liveblocks
dev server to test other features in your app._
## Set up the dev server
diff --git a/docs/references/v2.openapi.json b/docs/references/v2.openapi.json
index 7ccea9bb77..64270d55b2 100644
--- a/docs/references/v2.openapi.json
+++ b/docs/references/v2.openapi.json
@@ -1,7 +1,7 @@
{
"openapi": "3.1.0",
"info": {
- "title": "API v2",
+ "title": "Liveblocks API",
"version": "2.0"
},
"servers": [
@@ -13,61 +13,65 @@
"/rooms": {
"get": {
"summary": "Get rooms",
- "description": "This endpoint returns a list of your rooms. The rooms are returned sorted by creation date, from newest to oldest. You can filter rooms by room ID prefixes, metadata, users accesses, and groups accesses. Corresponds to [`liveblocks.getRooms`](/docs/api-reference/liveblocks-node#get-rooms).\n\nThere is a pagination system where the cursor to the next page is returned in the response as `nextCursor`, which can be combined with `startingAfter`.\nYou can also limit the number of rooms by query.\n\nFiltering by metadata works by giving key values like `metadata.color=red`. Of course you can combine multiple metadata clauses to refine the response like `metadata.color=red&metadata.type=text`. Notice here the operator AND is applied between each clauses.\n\nFiltering by groups or userId works by giving a list of groups like `groupIds=marketing,GZo7tQ,product` or/and a userId like `userId=user1`.\nNotice here the operator OR is applied between each `groupIds` and the `userId`.\n",
+ "description": "This endpoint returns a list of your rooms. The rooms are returned sorted by creation date, from newest to oldest. You can filter rooms by room ID prefixes, metadata, users accesses, and groups accesses. Corresponds to [`liveblocks.getRooms`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms).\n\nThere is a pagination system where the cursor to the next page is returned in the response as `nextCursor`, which can be combined with `startingAfter`.\nYou can also limit the number of rooms by query.\n\nFiltering by metadata works by giving key values like `metadata.color=red`. Of course you can combine multiple metadata clauses to refine the response like `metadata.color=red&metadata.type=text`. Notice here the operator AND is applied between each clauses.\n\nFiltering by groups or userId works by giving a list of groups like `groupIds=marketing,GZo7tQ,product` or/and a userId like `userId=user1`.\nNotice here the operator OR is applied between each `groupIds` and the `userId`.\n",
"tags": ["Room"],
"parameters": [
{
"schema": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 100,
- "default": 20
+ "default": 20,
+ "description": "A limit on the number of rooms to be returned. The limit can range between 1 and 100, and defaults to 20.",
+ "example": 20
},
"in": "query",
- "name": "limit",
- "description": "A limit on the number of rooms to be returned. The limit can range between 1 and 100, and defaults to 20."
+ "name": "limit"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.",
+ "example": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9"
},
"in": "query",
- "name": "startingAfter",
- "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page."
+ "name": "startingAfter"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A filter on organization ID.",
+ "example": "org_123456789"
},
"in": "query",
- "name": "organizationId",
- "description": "A filter on organization ID."
+ "name": "organizationId"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "Query to filter rooms. You can filter by `roomId` and `metadata`, for example, `metadata[\"roomType\"]:\"whiteboard\" AND roomId^\"liveblocks:engineering\"`. Learn more about [filtering rooms with query language](https://liveblocks.io/docs/guides/how-to-filter-rooms-using-query-language).",
+ "example": "metadata[\"color\"]:\"blue\""
},
"in": "query",
- "name": "query",
- "description": "Query to filter rooms. You can filter by `roomId` and `metadata`, for example, `metadata[\"roomType\"]:\"whiteboard\" AND roomId^\"liveblocks:engineering\"`. Learn more about [filtering rooms with query language](/docs/guides/how-to-filter-rooms-using-query-language)."
+ "name": "query"
},
{
"schema": {
"type": "string",
- "examples": ["userId=user1"]
+ "description": "A filter on users accesses.",
+ "example": "user-123"
},
"in": "query",
- "name": "userId",
- "description": "A filter on users accesses."
+ "name": "userId"
},
{
"schema": {
"type": "string",
- "examples": ["groupsIds=group1,group2"]
+ "description": "A filter on groups accesses. Multiple groups can be used.",
+ "example": "group1,group2"
},
"in": "query",
- "name": "groupIds",
- "description": "A filter on groups accesses. Multiple groups can be used."
+ "name": "groupIds"
}
],
"responses": {
@@ -76,7 +80,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/GetRooms"
+ "$ref": "#/components/schemas/GetRoomsResponse"
},
"examples": {
"example": {
@@ -88,6 +92,7 @@
"id": "HTOGSiXcORTECjfNBBLii",
"lastConnectionAt": "2022-08-08T23:23:15.281Z",
"createdAt": "2022-08-08T23:23:15.281Z",
+ "organizationId": "org_123456789",
"metadata": {
"name": ["My room"],
"type": ["whiteboard"]
@@ -121,9 +126,19 @@
},
"post": {
"summary": "Create room",
- "description": "This endpoint creates a new room. `id` and `defaultAccesses` are required. When provided with a `?idempotent` query argument, will not return a 409 when the room already exists, but instead return the existing room as-is. Corresponds to [`liveblocks.createRoom`](/docs/api-reference/liveblocks-node#post-rooms), or to [`liveblocks.getOrCreateRoom`](docs/api-reference/liveblocks-node#get-or-create-rooms-roomId) when `?idempotent` is provided. \n- `defaultAccessess` could be `[]` or `[\"room:write\"]` (private or public). \n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain 100 ids maximum. Id length has a limit of 40 characters. `usersAccesses` is optional field.\n- `groupsAccesses` are optional fields.\n",
+ "description": "This endpoint creates a new room. `id` and `defaultAccesses` are required. When provided with a `?idempotent` query argument, will not return a 409 when the room already exists, but instead return the existing room as-is. Corresponds to [`liveblocks.createRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms), or to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-create-rooms-roomId) when `?idempotent` is provided. \n- `defaultAccesses` could be `[]` or `[\"room:write\"]` (private or public). \n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.\n- `groupsAccesses` are optional fields.\n",
"tags": ["Room"],
- "parameters": [],
+ "parameters": [
+ {
+ "schema": {
+ "type": "boolean",
+ "description": "When provided, will not return a 409 when the room already exists, but instead return the existing room as-is. Corresponds to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-create-rooms-roomId).",
+ "example": true
+ },
+ "in": "query",
+ "name": "idempotent"
+ }
+ ],
"responses": {
"200": {
"description": "Success. Returns the created room.",
@@ -139,6 +154,7 @@
"id": "my-room-3ebc26e2bf96",
"lastConnectionAt": "2022-08-22T15:10:25.225Z",
"createdAt": "2022-08-22T15:10:25.225Z",
+ "organizationId": "org_123456789",
"metadata": {
"color": "blue"
},
@@ -168,12 +184,13 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-rooms",
+ "operationId": "create-room",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateRoom"
+ "$ref": "#/components/schemas/CreateRoomRequestBody"
},
"examples": {
"example": {
@@ -201,16 +218,17 @@
"get": {
"summary": "Get room",
"tags": ["Room"],
- "operationId": "get-rooms-roomId",
+ "operationId": "get-room",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
}
],
"responses": {
@@ -228,6 +246,7 @@
"id": "react-todo-list",
"lastConnectionAt": "2022-08-04T21:07:09.380Z",
"createdAt": "2022-07-13T14:32:50.697Z",
+ "organizationId": "org_123456789",
"metadata": {
"color": "blue",
"size": "10",
@@ -257,7 +276,7 @@
"$ref": "#/components/responses/404"
}
},
- "description": "This endpoint returns a room by its ID. Corresponds to [`liveblocks.getRoom`](/docs/api-reference/liveblocks-node#get-rooms-roomid)."
+ "description": "This endpoint returns a room by its ID. Corresponds to [`liveblocks.getRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomid)."
},
"post": {
"summary": "Update room",
@@ -265,12 +284,13 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
}
],
"responses": {
@@ -288,6 +308,7 @@
"id": "react-todo-list",
"lastConnectionAt": "2022-08-04T21:07:09.380Z",
"createdAt": "2022-07-13T14:32:50.697Z",
+ "organizationId": "org_123456789",
"metadata": {
"color": "blue",
"size": "10",
@@ -320,13 +341,14 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-rooms-roomId",
- "description": "This endpoint updates specific properties of a room. Corresponds to [`liveblocks.updateRoom`](/docs/api-reference/liveblocks-node#post-rooms-roomid). \n\nIt’s not necessary to provide the entire room’s information. \nSetting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users: \n``{\n \"usersAccesses\": {\n \"john\": null\n }\n}``\n`defaultAccessess`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.\n\n- `defaultAccessess` could be `[]` or `[\"room:write\"]` (private or public). \n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain 100 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.\n- `groupsAccesses` could be `[]` or `[\"room:write\"]` for every records. `groupsAccesses` can contain 100 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field.",
+ "operationId": "update-room",
+ "description": "This endpoint updates specific properties of a room. Corresponds to [`liveblocks.updateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomid). \n\nIt’s not necessary to provide the entire room’s information. \nSetting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users: \n``{\n \"usersAccesses\": {\n \"john\": null\n }\n}``\n`defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.\n\n- `defaultAccesses` could be `[]` or `[\"room:write\"]` (private or public). \n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.\n- `groupsAccesses` could be `[]` or `[\"room:write\"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field.",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpdateRoom"
+ "$ref": "#/components/schemas/UpdateRoomRequestBody"
},
"examples": {
"example": {
@@ -345,18 +367,6 @@
}
}
}
- },
- "application/xml": {
- "schema": {
- "type": "object",
- "properties": {}
- }
- },
- "multipart/form-data": {
- "schema": {
- "type": "object",
- "properties": {}
- }
}
}
}
@@ -456,32 +466,34 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
}
],
- "operationId": "delete-rooms-roomId",
- "description": "This endpoint deletes a room. A deleted room is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.deleteRoom`](/docs/api-reference/liveblocks-node#delete-rooms-roomid)."
+ "operationId": "delete-room",
+ "description": "This endpoint deletes a room. A deleted room is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.deleteRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomid)."
}
},
"/rooms/{roomId}/prewarm": {
"get": {
"summary": "Prewarm room",
"tags": ["Room"],
- "operationId": "get-rooms-roomId-prewarm",
+ "operationId": "prewarm-room",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
}
],
"responses": {
@@ -498,7 +510,7 @@
"$ref": "#/components/responses/404"
}
},
- "description": "Speeds up connecting to a room for the next 10 seconds. Use this when you know a user will be connecting to a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider) or [`enterRoom`](/docs/api-reference/liveblocks-client#Client.enterRoom) within 10 seconds, and the room will load quicker."
+ "description": "Speeds up connecting to a room for the next 10 seconds. Use this when you know a user will be connecting to a room with [`RoomProvider`](https://liveblocks.io/docs/api-reference/liveblocks-react#RoomProvider) or [`enterRoom`](https://liveblocks.io/docs/api-reference/liveblocks-client#Client.enterRoom) within 10 seconds, and the room will load quicker. Corresponds to [`liveblocks.prewarmRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomid-prewarm)."
}
},
"/rooms/{roomId}/upsert": {
@@ -508,12 +520,13 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
}
],
"responses": {
@@ -531,6 +544,7 @@
"id": "react-todo-list",
"lastConnectionAt": "2022-08-04T21:07:09.380Z",
"createdAt": "2022-07-13T14:32:50.697Z",
+ "organizationId": "org_123456789",
"metadata": {
"color": "blue",
"size": "10",
@@ -560,13 +574,14 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "upsert-rooms-roomId",
- "description": "This endpoint updates specific properties of a room. Corresponds to [`liveblocks.upsertRoom`](/docs/api-reference/liveblocks-node#upsert-rooms-roomId). \n\nIt’s not necessary to provide the entire room’s information. \nSetting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users: \n``{\n \"usersAccesses\": {\n \"john\": null\n }\n}``\n`defaultAccessess`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.\n\n- `defaultAccessess` could be `[]` or `[\"room:write\"]` (private or public). \n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain 100 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.\n- `groupsAccesses` could be `[]` or `[\"room:write\"]` for every records. `groupsAccesses` can contain 100 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field.",
+ "operationId": "upsert-room",
+ "description": "This endpoint updates specific properties of a room. Corresponds to [`liveblocks.upsertRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#upsert-rooms-roomId). \n\nIt’s not necessary to provide the entire room’s information. \nSetting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users: \n``{\n \"usersAccesses\": {\n \"john\": null\n }\n}``\n`defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.\n\n- `defaultAccesses` could be `[]` or `[\"room:write\"]` (private or public). \n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.\n- `groupsAccesses` could be `[]` or `[\"room:write\"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field.",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpsertRoom"
+ "$ref": "#/components/schemas/UpsertRoomRequestBody"
},
"examples": {
"example": {
@@ -589,18 +604,6 @@
}
}
}
- },
- "application/xml": {
- "schema": {
- "type": "object",
- "properties": {}
- }
- },
- "multipart/form-data": {
- "schema": {
- "type": "object",
- "properties": {}
- }
}
}
}
@@ -613,12 +616,13 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "The new ID for the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "The new ID for the room"
+ "required": true
}
],
"responses": {
@@ -636,6 +640,7 @@
"id": "react-todo-list",
"lastConnectionAt": "2022-08-04T21:07:09.380Z",
"createdAt": "2022-07-13T14:32:50.697Z",
+ "organizationId": "org_123456789",
"metadata": {
"color": "blue",
"size": "10",
@@ -668,11 +673,14 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-rooms-update-roomId",
- "description": "This endpoint permanently updates the room’s ID.",
+ "operationId": "update-room-id",
+ "description": "This endpoint permanently updates the room’s ID. All existing references to the old room ID will need to be updated. Returns the updated room. Corresponds to [`liveblocks.updateRoomId`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomid-update-room-id).",
"requestBody": {
"content": {
"application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpdateRoomIdRequestBody"
+ },
"examples": {
"example": {
"value": {
@@ -680,18 +688,6 @@
}
}
}
- },
- "application/xml": {
- "schema": {
- "type": "object",
- "properties": {}
- }
- },
- "multipart/form-data": {
- "schema": {
- "type": "object",
- "properties": {}
- }
}
}
}
@@ -701,17 +697,18 @@
"get": {
"summary": "Get active users",
"tags": ["Room"],
- "operationId": "get-rooms-roomId-active-users",
- "description": "This endpoint returns a list of users currently present in the requested room. Corresponds to [`liveblocks.getActiveUsers`](/docs/api-reference/liveblocks-node#get-rooms-roomid-active-users). \n\nFor optimal performance, we recommend calling this endpoint no more than once every 10 seconds. \nDuplicates can occur if a user is in the requested room with multiple browser tabs opened.",
+ "operationId": "get-active-users",
+ "description": "This endpoint returns a list of users currently present in the requested room. Corresponds to [`liveblocks.getActiveUsers`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomid-active-users). \n\nFor optimal performance, we recommend calling this endpoint no more than once every 10 seconds. \nDuplicates can occur if a user is in the requested room with multiple browser tabs opened.",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
}
],
"responses": {
@@ -760,18 +757,19 @@
"/rooms/{roomId}/presence": {
"post": {
"summary": "Set ephemeral presence",
- "description": "This endpoint sets ephemeral presence for a user in a room without requiring a WebSocket connection. The presence data will automatically expire after the specified TTL (time-to-live). This is useful for scenarios like showing an AI agent's presence in a room. The presence will be broadcast to all connected users in the room.",
+ "description": "This endpoint sets ephemeral presence for a user in a room without requiring a WebSocket connection. The presence data will automatically expire after the specified TTL (time-to-live). This is useful for scenarios like showing an AI agent's presence in a room. The presence will be broadcast to all connected users in the room. Corresponds to [`liveblocks.setPresence`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-presence).",
"tags": ["Room"],
- "operationId": "post-rooms-roomId-presence",
+ "operationId": "set-presence",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
}
],
"requestBody": {
@@ -779,7 +777,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SetPresence"
+ "$ref": "#/components/schemas/SetPresenceRequestBody"
},
"examples": {
"example": {
@@ -825,20 +823,21 @@
"/rooms/{roomId}/broadcast_event": {
"post": {
"summary": "Broadcast event to a room",
- "description": "This endpoint enables the broadcast of an event to a room without having to connect to it via the `client` from `@liveblocks/client`. It takes any valid JSON as a request body. The `connectionId` passed to event listeners is `-1` when using this API. Corresponds to [`liveblocks.broadcastEvent`](/docs/api-reference/liveblocks-node#post-broadcast-event).",
+ "description": "This endpoint enables the broadcast of an event to a room without having to connect to it via the `client` from `@liveblocks/client`. It takes any valid JSON as a request body. The `connectionId` passed to event listeners is `-1` when using this API. Corresponds to [`liveblocks.broadcastEvent`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-broadcast-event).",
"tags": ["Room"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
}
],
- "operationId": "post-broadcast-event",
+ "operationId": "broadcast-event",
"responses": {
"204": {
"description": "Success. An event was broadcast to the room."
@@ -857,10 +856,11 @@
}
},
"requestBody": {
+ "required": true,
"description": "Any valid JSON.",
"content": {
- "schema": {},
"application/json": {
+ "schema": {},
"examples": {
"example": {
"value": {
@@ -877,27 +877,29 @@
"/rooms/{roomId}/storage": {
"get": {
"summary": "Get Storage document",
- "description": "Returns the contents of the room’s Storage tree. Corresponds to [`liveblocks.getStorageDocument`](/docs/api-reference/liveblocks-node#get-rooms-roomId-storage). \n\nThe default outputted format is called “plain LSON”, which includes information on the Live data structures in the tree. These nodes show up in the output as objects with two properties, for example:\n\n```json\n{\n \"liveblocksType\": \"LiveObject\",\n \"data\": ...\n}\n```\n\nIf you’re not interested in this information, you can use the simpler `?format=json` query param, see below.",
+ "description": "Returns the contents of the room’s Storage tree. Corresponds to [`liveblocks.getStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-storage). \n\nThe default outputted format is called “plain LSON”, which includes information on the Live data structures in the tree. These nodes show up in the output as objects with two properties, for example:\n\n```json\n{\n \"liveblocksType\": \"LiveObject\",\n \"data\": ...\n}\n```\n\nIf you’re not interested in this information, you can use the simpler `?format=json` query param, see below.",
"tags": ["Storage"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
},
{
"schema": {
"type": "string",
- "enum": ["plain-lson", "json"]
+ "enum": ["plain-lson", "json"],
+ "description": "Use the `json` format to output a simplified JSON representation of the Storage tree. In that format, each LiveObject and LiveMap will be formatted as a simple JSON object, and each LiveList will be formatted as a simple JSON array. This is a lossy format because information about the original data structures is not retained, but it may be easier to work with.",
+ "example": "json"
},
"name": "format",
"in": "query",
- "required": false,
- "description": "Use `?format=json` to output a simplified JSON representation of the Storage tree. In that format, each LiveObject and LiveMap will be formatted as a simple JSON object, and each LiveList will be formatted as a simple JSON array. This is a lossy format because information about the original data structures is not retained, but it may be easier to work with."
+ "required": false
}
],
"responses": {
@@ -906,15 +908,7 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "liveblocksType": {
- "type": "string"
- },
- "data": {
- "type": ["string", "object"]
- }
- }
+ "$ref": "#/components/schemas/GetStorageDocumentResponse"
},
"examples": {
"example": {
@@ -955,21 +949,22 @@
"$ref": "#/components/responses/404"
}
},
- "operationId": "get-rooms-roomId-storage"
+ "operationId": "get-storage-document"
},
"post": {
"summary": "Initialize Storage document",
"tags": ["Storage"],
- "operationId": "post-rooms-roomId-storage",
+ "operationId": "initialize-storage-document",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
}
],
"responses": {
@@ -978,15 +973,7 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "liveblocksType": {
- "type": "string"
- },
- "data": {
- "type": ["string", "object"]
- }
- }
+ "$ref": "#/components/schemas/InitializeStorageDocumentResponse"
},
"examples": {
"example": {
@@ -1047,7 +1034,7 @@
}
}
},
- "description": "This endpoint initializes or reinitializes a room’s Storage. The room must already exist. Calling this endpoint will disconnect all users from the room if there are any, triggering a reconnect. Corresponds to [`liveblocks.initializeStorageDocument`](/docs/api-reference/liveblocks-node#post-rooms-roomId-storage).\n\nThe format of the request body is the same as what’s returned by the get Storage endpoint.\n\nFor each Liveblocks data structure that you want to create, you need a JSON element having two properties:\n- `\"liveblocksType\"` => `\"LiveObject\" | \"LiveList\" | \"LiveMap\"`\n- `\"data\"` => contains the nested data structures (children) and data.\n\nThe root’s type can only be LiveObject.\n\nA utility function, `toPlainLson` is included in `@liveblocks/client` from `1.0.9` to help convert `LiveObject`, `LiveList`, and `LiveMap` to the structure expected by the endpoint.",
+ "description": "This endpoint initializes or reinitializes a room’s Storage. The room must already exist. Calling this endpoint will disconnect all users from the room if there are any, triggering a reconnect. Corresponds to [`liveblocks.initializeStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-storage).\n\nThe format of the request body is the same as what’s returned by the get Storage endpoint.\n\nFor each Liveblocks data structure that you want to create, you need a JSON element having two properties:\n- `\"liveblocksType\"` => `\"LiveObject\" | \"LiveList\" | \"LiveMap\"`\n- `\"data\"` => contains the nested data structures (children) and data.\n\nThe root’s type can only be LiveObject.\n\nA utility function, `toPlainLson` is included in `@liveblocks/client` from `1.0.9` to help convert `LiveObject`, `LiveList`, and `LiveMap` to the structure expected by the endpoint.",
"requestBody": {
"content": {
"application/json": {
@@ -1055,10 +1042,12 @@
"type": "object",
"properties": {
"liveblocksType": {
- "type": "string"
+ "type": "string",
+ "const": "LiveObject"
},
"data": {
- "type": "object"
+ "type": "object",
+ "additionalProperties": true
}
}
},
@@ -1095,16 +1084,17 @@
"delete": {
"summary": "Delete Storage document",
"tags": ["Storage"],
- "operationId": "delete-rooms-roomId-storage",
+ "operationId": "delete-storage-document",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
}
],
"responses": {
@@ -1121,24 +1111,25 @@
"$ref": "#/components/responses/404"
}
},
- "description": "This endpoint deletes all of the room’s Storage data. Calling this endpoint will disconnect all users from the room if there are any. Corresponds to [`liveblocks.deleteStorageDocument`](/docs/api-reference/liveblocks-node#delete-rooms-roomId-storage).\n"
+ "description": "This endpoint deletes all of the room’s Storage data. Calling this endpoint will disconnect all users from the room if there are any. Corresponds to [`liveblocks.deleteStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-storage).\n"
}
},
"/rooms/{roomId}/storage/json-patch": {
"patch": {
"summary": "Apply JSON Patch to Storage",
- "description": "Applies a sequence of [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations to the room's Storage document, useful for modifying Storage. Operations are applied in order; if any operation fails, the document is not changed and a 422 response with a helpful message is returned.\n\n**Paths and data types:** Be as specific as possible with your target path. Every parent in the chain of path segments must be a LiveObject, LiveList, or LiveMap. Complex nested objects passed in `add` or `replace` operations are automatically converted to LiveObjects and LiveLists.\n\n**Performance:** For large Storage documents, applying a patch can be expensive because the full state is reconstructed on the server to apply the operations. Very large documents may not be suitable for this endpoint.\n\nFor a **full guide with examples**, see [Modifying storage via REST API with JSON Patch](/docs/guides/modifying-storage-via-rest-api-with-json-patch).",
+ "description": "Applies a sequence of [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations to the room's Storage document, useful for modifying Storage. Operations are applied in order; if any operation fails, the document is not changed and a 422 response with a helpful message is returned.\n\n**Paths and data types:** Be as specific as possible with your target path. Every parent in the chain of path segments must be a LiveObject, LiveList, or LiveMap. Complex nested objects passed in `add` or `replace` operations are automatically converted to LiveObjects and LiveLists.\n\n**Performance:** For large Storage documents, applying a patch can be expensive because the full state is reconstructed on the server to apply the operations. Very large documents may not be suitable for this endpoint.\n\nFor a **full guide with examples**, see [Modifying storage via REST API with JSON Patch](https://liveblocks.io/docs/guides/modifying-storage-via-rest-api-with-json-patch).",
"tags": ["Storage"],
- "operationId": "patch-rooms-roomId-storage-json-patch",
+ "operationId": "patch-storage-document",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
}
],
"requestBody": {
@@ -1146,50 +1137,31 @@
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "type": "object",
- "required": ["op", "path"],
- "properties": {
- "op": {
- "type": "string",
- "enum": [
- "add",
- "remove",
- "replace",
- "move",
- "copy",
- "test"
- ],
- "description": "The operation to perform (RFC 6902)."
- },
- "path": {
- "type": "string",
- "description": "A JSON Pointer to the target location (RFC 6901). Must start with \"/\"."
- },
- "from": {
- "type": "string",
- "description": "Required for \"move\" and \"copy\". A JSON Pointer to the source location."
- },
- "value": {
- "description": "Required for \"add\", \"replace\", and \"test\". The value to add, the replacement value, or the value to test against."
- }
- },
- "additionalProperties": true
- }
+ "$ref": "#/components/schemas/PatchStorageDocumentRequestBody"
},
"examples": {
"addAndRemove": {
"summary": "Add and remove",
"value": [
- { "op": "add", "path": "/score", "value": 42 },
- { "op": "remove", "path": "/oldKey" }
+ {
+ "op": "add",
+ "path": "/score",
+ "value": 42
+ },
+ {
+ "op": "remove",
+ "path": "/oldKey"
+ }
]
},
"appendToList": {
"summary": "Append to LiveList",
"value": [
- { "op": "add", "path": "/layers/-", "value": "newLayer" }
+ {
+ "op": "add",
+ "path": "/layers/-",
+ "value": "newLayer"
+ }
]
}
}
@@ -1243,7 +1215,7 @@
"/rooms/{roomId}/ydoc": {
"get": {
"summary": "Get Yjs document",
- "description": "This endpoint returns a JSON representation of the room’s Yjs document. Corresponds to [`liveblocks.getYjsDocument`](/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc).",
+ "description": "This endpoint returns a JSON representation of the room’s Yjs document. Corresponds to [`liveblocks.getYjsDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc).",
"tags": ["Yjs"],
"parameters": [
{
@@ -1251,38 +1223,41 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"schema": {
- "type": "boolean"
+ "type": "boolean",
+ "description": "If present, YText will return formatting."
},
"name": "formatting",
"in": "query",
"allowEmptyValue": true,
- "required": false,
- "description": "If present, YText will return formatting."
+ "required": false
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "Returns only a single key’s value, e.g. `doc.get(key).toJSON()`.",
+ "example": "root"
},
"name": "key",
"in": "query",
- "required": false,
- "description": "Returns only a single key’s value, e.g. `doc.get(key).toJSON()`."
+ "required": false
},
{
"schema": {
"type": "string",
- "enum": ["ymap", "ytext", "yxmltext", "yxmlfragment", "yarray"]
+ "enum": ["ymap", "ytext", "yxmltext", "yxmlfragment", "yarray"],
+ "description": "Used with key to override the inferred type, i.e. `\"ymap\"` will return `doc.get(key, Y.Map)`.",
+ "example": "ymap"
},
"name": "type",
"in": "query",
- "required": false,
- "description": "Used with key to override the inferred type, i.e. `\"ymap\"` will return `doc.get(key, Y.Map)`."
+ "required": false
}
],
"responses": {
@@ -1291,7 +1266,7 @@
"content": {
"application/json": {
"schema": {
- "type": "object"
+ "$ref": "#/components/schemas/GetYjsDocumentResponse"
},
"examples": {
"example": {
@@ -1313,7 +1288,7 @@
"$ref": "#/components/responses/404"
}
},
- "operationId": "get-rooms-roomId-ydoc"
+ "operationId": "get-yjs-document"
},
"put": {
"summary": "Send a binary Yjs update",
@@ -1321,33 +1296,28 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
},
{
"name": "guid",
"in": "query",
"required": false,
"schema": {
- "type": "string"
- },
- "description": "ID of the subdocument"
+ "type": "string",
+ "description": "ID of the subdocument",
+ "example": "subdoc-guid-123"
+ }
}
],
"responses": {
"200": {
- "description": "Success. The given room’s Yjs doc has been updated.",
- "content": {
- "application/json": {
- "schema": {
- "status": 200
- }
- }
- }
+ "description": "Success. The given room’s Yjs doc has been updated."
},
"401": {
"$ref": "#/components/responses/401"
@@ -1359,9 +1329,10 @@
"$ref": "#/components/responses/404"
}
},
- "operationId": "put-rooms-roomId-ydoc",
- "description": "This endpoint is used to send a Yjs binary update to the room’s Yjs document. You can use this endpoint to initialize Yjs data for the room or to update the room’s Yjs document. To send an update to a subdocument instead of the main document, pass its `guid`. Corresponds to [`liveblocks.sendYjsBinaryUpdate`](/docs/api-reference/liveblocks-node#put-rooms-roomId-ydoc).\n\nThe update is typically obtained by calling `Y.encodeStateAsUpdate(doc)`. See the [Yjs documentation](https://docs.yjs.dev/api/document-updates) for more details. When manually making this HTTP call, set the HTTP header `Content-Type` to `application/octet-stream`, and send the binary update (a `Uint8Array`) in the body of the HTTP request. This endpoint does not accept JSON, unlike most other endpoints.",
+ "operationId": "send-yjs-binary-update",
+ "description": "This endpoint is used to send a Yjs binary update to the room’s Yjs document. You can use this endpoint to initialize Yjs data for the room or to update the room’s Yjs document. To send an update to a subdocument instead of the main document, pass its `guid`. Corresponds to [`liveblocks.sendYjsBinaryUpdate`](https://liveblocks.io/docs/api-reference/liveblocks-node#put-rooms-roomId-ydoc).\n\nThe update is typically obtained by calling `Y.encodeStateAsUpdate(doc)`. See the [Yjs documentation](https://docs.yjs.dev/api/document-updates) for more details. When manually making this HTTP call, set the HTTP header `Content-Type` to `application/octet-stream`, and send the binary update (a `Uint8Array`) in the body of the HTTP request. This endpoint does not accept JSON, unlike most other endpoints.",
"requestBody": {
+ "required": true,
"content": {
"application/octet-stream": {
"schema": {
@@ -1376,7 +1347,7 @@
"/rooms/{roomId}/ydoc-binary": {
"get": {
"summary": "Get Yjs document encoded as a binary Yjs update",
- "description": "This endpoint returns the room's Yjs document encoded as a single binary update. This can be used by `Y.applyUpdate(responseBody)` to get a copy of the document in your back end. See [Yjs documentation](https://docs.yjs.dev/api/document-updates) for more information on working with updates. To return a subdocument instead of the main document, pass its `guid`. Corresponds to [`liveblocks.getYjsDocumentAsBinaryUpdate`](/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc-binary).",
+ "description": "This endpoint returns the room's Yjs document encoded as a single binary update. This can be used by `Y.applyUpdate(responseBody)` to get a copy of the document in your back end. See [Yjs documentation](https://docs.yjs.dev/api/document-updates) for more information on working with updates. To return a subdocument instead of the main document, pass its `guid`. Corresponds to [`liveblocks.getYjsDocumentAsBinaryUpdate`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc-binary).",
"tags": ["Yjs"],
"parameters": [
{
@@ -1384,18 +1355,20 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "guid",
"in": "query",
"required": false,
"schema": {
- "type": "string"
- },
- "description": "ID of the subdocument"
+ "type": "string",
+ "description": "ID of the subdocument",
+ "example": "subdoc-guid-123"
+ }
}
],
"responses": {
@@ -1420,7 +1393,7 @@
"$ref": "#/components/responses/404"
}
},
- "operationId": "get-rooms-roomId-ydoc-binary"
+ "operationId": "get-yjs-document-as-binary-update"
}
},
"/rooms/{roomId}/versions": {
@@ -1428,35 +1401,38 @@
"summary": "Get Yjs version history",
"description": "This endpoint returns a list of version history snapshots for the room's Yjs document. The versions are returned sorted by creation date, from newest to oldest.",
"tags": ["Yjs"],
- "operationId": "get-rooms-roomId-versions",
+ "operationId": "get-yjs-versions",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"schema": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 100,
- "default": 20
+ "default": 20,
+ "description": "A limit on the number of versions to be returned. The limit can range between 1 and 100, and defaults to 20.",
+ "example": 20
},
"in": "query",
- "name": "limit",
- "description": "A limit on the number of versions to be returned. The limit can range between 1 and 100, and defaults to 20."
+ "name": "limit"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.",
+ "example": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9"
},
"in": "query",
- "name": "cursor",
- "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page."
+ "name": "cursor"
}
],
"responses": {
@@ -1465,7 +1441,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/GetYjsVersions"
+ "$ref": "#/components/schemas/GetYjsVersionsResponse"
},
"examples": {
"example": {
@@ -1510,25 +1486,27 @@
"summary": "Get Yjs document version",
"description": "This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update.",
"tags": ["Yjs"],
- "operationId": "get-rooms-roomId-version-versionId",
+ "operationId": "get-yjs-version",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "versionId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the version"
+ "type": "string",
+ "description": "ID of the version",
+ "example": "vh_abc123"
+ }
}
],
"responses": {
@@ -1560,16 +1538,17 @@
"summary": "Create Yjs version snapshot",
"description": "This endpoint creates a new version history snapshot for the room's Yjs document.",
"tags": ["Yjs"],
- "operationId": "post-rooms-roomId-version",
+ "operationId": "create-yjs-version",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
}
],
"responses": {
@@ -1578,7 +1557,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateYjsVersion"
+ "$ref": "#/components/schemas/CreateYjsVersionResponse"
},
"examples": {
"example": {
@@ -1607,7 +1586,7 @@
"/rooms/{roomId}/threads": {
"get": {
"summary": "Get room threads",
- "description": "This endpoint returns the threads in the requested room. Corresponds to [`liveblocks.getThreads`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads).",
+ "description": "This endpoint returns the threads in the requested room. Corresponds to [`liveblocks.getThreads`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads).",
"tags": ["Comments"],
"parameters": [
{
@@ -1615,35 +1594,29 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "Query to filter threads. You can filter by `metadata` and `resolved`, for example, `metadata[\"status\"]:\"open\" AND metadata[\"color\"]:\"red\" AND resolved:true`. Learn more about [filtering threads with query language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language).",
+ "example": "metadata[\"color\"]:\"blue\""
},
"in": "query",
- "name": "query",
- "description": "Query to filter threads. You can filter by `metadata` and `resolved`, for example, `metadata[\"status\"]:\"open\" AND metadata[\"color\"]:\"red\" AND resolved:true`. Learn more about [filtering threads with query language](/docs/guides/how-to-filter-threads-using-query-language)."
+ "name": "query"
}
],
- "operationId": "get-rooms-roomId-threads",
+ "operationId": "get-threads",
"responses": {
"200": {
"description": "Success. Returns list of threads in a room.",
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Thread"
- }
- }
- }
+ "$ref": "#/components/schemas/GetThreadsResponse"
},
"examples": {
"Success": {
@@ -1693,18 +1666,19 @@
},
"post": {
"summary": "Create thread",
- "description": "This endpoint creates a new thread and the first comment in the thread. Corresponds to [`liveblocks.createThread`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads).\n\nA comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `comment.body`.\n\n```json\n\"version\": 1,\n\"content\": [\n {\n \"type\": \"paragraph\",\n \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]\n }\n]\n```",
+ "description": "This endpoint creates a new thread and the first comment in the thread. Corresponds to [`liveblocks.createThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads).\n\nA comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `comment.body`.\n\n```json\n{\n \"version\": 1,\n \"content\": [\n {\n \"type\": \"paragraph\",\n \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]\n }\n ]\n}\n```\n\n`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.",
"tags": ["Comments"],
- "operationId": "post-rooms-roomId-threads",
+ "operationId": "create-thread",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
}
],
"responses": {
@@ -1756,10 +1730,11 @@
}
},
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateThread"
+ "$ref": "#/components/schemas/CreateThreadRequestBody"
},
"examples": {
"example": {
@@ -1791,26 +1766,28 @@
"get": {
"summary": "Get thread",
"tags": ["Comments"],
- "description": "This endpoint returns a thread by its ID. Corresponds to [`liveblocks.getThread`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId).",
- "operationId": "get-rooms-roomId-threads-threadId",
+ "description": "This endpoint returns a thread by its ID. Corresponds to [`liveblocks.getThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId).",
+ "operationId": "get-thread",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
}
],
"responses": {
@@ -1866,26 +1843,28 @@
"delete": {
"summary": "Delete thread",
"tags": ["Comments"],
- "description": "This endpoint deletes a thread by its ID. Corresponds to [`liveblocks.deleteThread`](/docs/api-reference/liveblocks-node#delete-rooms-roomId-threads-threadId).",
- "operationId": "delete-rooms-roomId-threads-threadId",
+ "description": "This endpoint deletes a thread by its ID. Corresponds to [`liveblocks.deleteThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-threads-threadId).",
+ "operationId": "delete-thread",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
}
],
"responses": {
@@ -1908,27 +1887,30 @@
"get": {
"summary": "Get thread participants",
"x-badge": "Deprecated",
- "tags": ["Deprecated"],
- "description": "**Deprecated.** Prefer using [thread subscriptions](#get-rooms-roomId-threads-threadId-subscriptions) instead.\n\nThis endpoint returns the list of thread participants. It is a list of unique user IDs representing all the thread comment authors and mentioned users in comments. Corresponds to [`liveblocks.getThreadParticipants`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-participants).",
- "operationId": "get-rooms-roomId-threads-threadId-participants",
+ "tags": ["Comments"],
+ "deprecated": true,
+ "description": "**Deprecated.** Prefer using [thread subscriptions](#get-rooms-roomId-threads-threadId-subscriptions) instead.\n\nThis endpoint returns the list of thread participants. It is a list of unique user IDs representing all the thread comment authors and mentioned users in comments. Corresponds to [`liveblocks.getThreadParticipants`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-participants).",
+ "operationId": "get-thread-participants",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
}
],
"responses": {
@@ -1937,15 +1919,7 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "participantIds": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- }
+ "$ref": "#/components/schemas/GetThreadParticipantsResponse"
},
"examples": {
"Success": {
@@ -1973,26 +1947,28 @@
"post": {
"summary": "Edit thread metadata",
"tags": ["Comments"],
- "description": "This endpoint edits the metadata of a thread. The metadata is a JSON object that can be used to store any information you want about the thread, in `string`, `number`, or `boolean` form. Set a property to `null` to remove it. Corresponds to [`liveblocks.editThreadMetadata`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-metadata).",
- "operationId": "post-rooms-roomId-threads-threadId-metadata",
+ "description": "This endpoint edits the metadata of a thread. The metadata is a JSON object that can be used to store any information you want about the thread, in `string`, `number`, or `boolean` form. Set a property to `null` to remove it. Corresponds to [`liveblocks.editThreadMetadata`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-metadata).\n\n`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.",
+ "operationId": "edit-thread-metadata",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
}
],
"responses": {
@@ -2021,10 +1997,11 @@
}
},
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpdateThreadMetadata"
+ "$ref": "#/components/schemas/EditThreadMetadataRequestBody"
},
"examples": {
"example": {
@@ -2046,28 +2023,40 @@
"post": {
"summary": "Mark thread as resolved",
"tags": ["Comments"],
- "description": "This endpoint marks a thread as resolved.",
- "operationId": "post-rooms-roomId-threads-threadId-mark-as-resolved",
+ "description": "This endpoint marks a thread as resolved. The request body must include a `userId` to identify who resolved the thread. Returns the updated thread. Corresponds to [`liveblocks.markThreadAsResolved`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-mark-as-resolved).",
+ "operationId": "mark-thread-as-resolved",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
}
],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MarkThreadAsResolvedRequestBody"
+ }
+ }
+ }
+ },
"responses": {
"200": {
"description": "Success. Returns the updated thread.",
@@ -2092,28 +2081,40 @@
"post": {
"summary": "Mark thread as unresolved",
"tags": ["Comments"],
- "description": "This endpoint marks a thread as unresolved.",
- "operationId": "post-rooms-roomId-threads-threadId-mark-as-unresolved",
+ "description": "This endpoint marks a thread as unresolved. The request body must include a `userId` to identify who unresolved the thread. Returns the updated thread. Corresponds to [`liveblocks.markThreadAsUnresolved`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-mark-as-unresolved).",
+ "operationId": "mark-thread-as-unresolved",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
}
],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MarkThreadAsUnresolvedRequestBody"
+ }
+ }
+ }
+ },
"responses": {
"200": {
"description": "Success. Returns the updated thread.",
@@ -2138,26 +2139,28 @@
"post": {
"summary": "Subscribe to thread",
"tags": ["Comments"],
- "description": "This endpoint subscribes to a thread. Corresponds to [`liveblocks.subscribeToThread`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-subscribe).",
- "operationId": "post-rooms-roomId-threads-threadId-subscribe",
+ "description": "This endpoint subscribes to a thread. Corresponds to [`liveblocks.subscribeToThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-subscribe).",
+ "operationId": "subscribe-to-thread",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
}
],
"requestBody": {
@@ -2165,13 +2168,7 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "required": ["userId"],
- "properties": {
- "userId": {
- "type": "string"
- }
- }
+ "$ref": "#/components/schemas/SubscribeToThreadRequestBody"
}
}
}
@@ -2198,7 +2195,8 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
@@ -2206,7 +2204,8 @@
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "th_abc123"
},
"name": "threadId",
"in": "path",
@@ -2218,26 +2217,28 @@
"post": {
"summary": "Unsubscribe from thread",
"tags": ["Comments"],
- "description": "This endpoint unsubscribes from a thread. Corresponds to [`liveblocks.unsubscribeFromThread`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-unsubscribe).",
- "operationId": "post-rooms-roomId-threads-threadId-unsubscribe",
+ "description": "This endpoint unsubscribes from a thread. Corresponds to [`liveblocks.unsubscribeFromThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-unsubscribe).",
+ "operationId": "unsubscribe-from-thread",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
}
],
"requestBody": {
@@ -2245,28 +2246,14 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "required": ["userId"],
- "properties": {
- "userId": {
- "type": "string"
- }
- }
+ "$ref": "#/components/schemas/UnsubscribeFromThreadRequestBody"
}
}
}
},
"responses": {
"200": {
- "description": "Success.",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {}
- }
- }
- }
+ "description": "Success."
},
"403": {
"$ref": "#/components/responses/403"
@@ -2279,7 +2266,8 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
@@ -2287,7 +2275,8 @@
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "th_abc123"
},
"name": "threadId",
"in": "path",
@@ -2299,26 +2288,28 @@
"get": {
"summary": "Get thread subscriptions",
"tags": ["Comments"],
- "description": "This endpoint gets the list of subscriptions to a thread. Corresponds to [`liveblocks.getThreadSubscriptions`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-subscriptions).",
- "operationId": "get-rooms-roomId-threads-threadId-subscriptions",
+ "description": "This endpoint gets the list of subscriptions to a thread. Corresponds to [`liveblocks.getThreadSubscriptions`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-subscriptions).",
+ "operationId": "get-thread-subscriptions",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
}
],
"responses": {
@@ -2327,15 +2318,7 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/UserSubscription"
- }
- }
- }
+ "$ref": "#/components/schemas/GetThreadSubscriptionsResponse"
}
}
}
@@ -2352,7 +2335,8 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
@@ -2360,7 +2344,8 @@
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "th_abc123"
},
"name": "threadId",
"in": "path",
@@ -2371,27 +2356,29 @@
"/rooms/{roomId}/threads/{threadId}/comments": {
"post": {
"summary": "Create comment",
- "description": "This endpoint creates a new comment, adding it as a reply to a thread. Corresponds to [`liveblocks.createComment`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments).\n\nA comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `body`.\n\n```json\n\"version\": 1,\n\"content\": [\n {\n \"type\": \"paragraph\",\n \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]\n }\n]\n",
+ "description": "This endpoint creates a new comment, adding it as a reply to a thread. Corresponds to [`liveblocks.createComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments).\n\nA comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `body`.\n\n```json\n{\n \"version\": 1,\n \"content\": [\n {\n \"type\": \"paragraph\",\n \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]\n }\n ]\n}\n```\n\n`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.",
"tags": ["Comments"],
- "operationId": "post-rooms-roomId-threads-threadId-comments",
+ "operationId": "create-comment",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
}
],
"responses": {
@@ -2435,10 +2422,11 @@
}
},
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpdateComment"
+ "$ref": "#/components/schemas/CreateCommentRequestBody"
},
"examples": {
"example": {
@@ -2465,35 +2453,38 @@
"get": {
"summary": "Get comment",
"tags": ["Comments"],
- "description": "This endpoint returns a comment by its ID. Corresponds to [`liveblocks.getComment`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-comments-commentId).",
- "operationId": "get-rooms-roomId-threads-threadId-comments-commentId",
+ "description": "This endpoint returns a comment by its ID. Corresponds to [`liveblocks.getComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-comments-commentId).",
+ "operationId": "get-comment",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
},
{
"name": "commentId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the comment"
+ "type": "string",
+ "description": "ID of the comment",
+ "example": "cm_abc123"
+ }
}
],
"responses": {
@@ -2538,36 +2529,39 @@
},
"post": {
"summary": "Edit comment",
- "description": "This endpoint edits the specified comment. Corresponds to [`liveblocks.editComment`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId).\n\nA comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `body`.\n\n```json\n\"version\": 1,\n\"content\": [\n {\n \"type\": \"paragraph\",\n \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]\n }\n]\n",
+ "description": "This endpoint edits the specified comment. Corresponds to [`liveblocks.editComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId).\n\nA comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `body`.\n\n```json\n{\n \"version\": 1,\n \"content\": [\n {\n \"type\": \"paragraph\",\n \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]\n }\n ]\n}\n```",
"tags": ["Comments"],
- "operationId": "post-rooms-roomId-threads-threadId-comments-commentId",
+ "operationId": "edit-comment",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
},
{
"name": "commentId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the comment"
+ "type": "string",
+ "description": "ID of the comment",
+ "example": "cm_abc123"
+ }
}
],
"responses": {
@@ -2576,7 +2570,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpdateComment"
+ "$ref": "#/components/schemas/Comment"
},
"examples": {
"example": {
@@ -2611,10 +2605,11 @@
}
},
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpdateComment"
+ "$ref": "#/components/schemas/EditCommentRequestBody"
},
"examples": {
"example": {
@@ -2640,27 +2635,30 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
},
{
"name": "commentId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the comment"
+ "type": "string",
+ "description": "ID of the comment",
+ "example": "cm_abc123"
+ }
}
],
"responses": {
@@ -2677,43 +2675,46 @@
"$ref": "#/components/responses/404-comment"
}
},
- "operationId": "delete-rooms-roomId-threads-threadId-comments-commentId",
- "description": "This endpoint deletes a comment. A deleted comment is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.deleteComment`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId)."
+ "operationId": "delete-comment",
+ "description": "This endpoint deletes a comment. A deleted comment is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.deleteComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId)."
}
},
"/rooms/{roomId}/threads/{threadId}/comments/{commentId}/add-reaction": {
"post": {
"summary": "Add comment reaction",
- "description": "This endpoint adds a reaction to a comment. Corresponds to [`liveblocks.addCommentReaction`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction).",
+ "description": "This endpoint adds a reaction to a comment. Corresponds to [`liveblocks.addCommentReaction`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction).",
"tags": ["Comments"],
- "operationId": "post-rooms-roomId-threads-threadId-comments-commentId-add-reaction",
+ "operationId": "add-comment-reaction",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
},
{
"name": "commentId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the comment"
+ "type": "string",
+ "description": "ID of the comment",
+ "example": "cm_abc123"
+ }
}
],
"responses": {
@@ -2750,10 +2751,11 @@
}
},
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/AddCommentReaction"
+ "$ref": "#/components/schemas/AddCommentReactionRequestBody"
},
"examples": {
"example": {
@@ -2772,36 +2774,39 @@
"/rooms/{roomId}/threads/{threadId}/comments/{commentId}/remove-reaction": {
"post": {
"summary": "Remove comment reaction",
- "description": "This endpoint removes a comment reaction. A deleted comment reaction is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.removeCommentReaction`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction).",
+ "description": "This endpoint removes a comment reaction. A deleted comment reaction is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.removeCommentReaction`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction).",
"tags": ["Comments"],
- "operationId": "post-rooms-roomId-threads-threadId-comments-commentId-remove-reaction",
+ "operationId": "remove-comment-reaction",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
},
{
"name": "commentId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the comment"
+ "type": "string",
+ "description": "ID of the comment",
+ "example": "cm_abc123"
+ }
}
],
"responses": {
@@ -2822,7 +2827,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/RemoveCommentReaction"
+ "$ref": "#/components/schemas/RemoveCommentReactionRequestBody"
},
"examples": {
"example": {
@@ -2842,35 +2847,38 @@
"post": {
"summary": "Edit comment metadata",
"tags": ["Comments"],
- "description": "This endpoint edits the metadata of a comment. The metadata is a JSON object that can be used to store any information you want about the comment, in `string`, `number`, or `boolean` form. Set a property to `null` to remove it. Corresponds to [`liveblocks.editCommentMetadata`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-metadata).",
- "operationId": "post-rooms-roomId-threads-threadId-comments-commentId-metadata",
+ "description": "This endpoint edits the metadata of a comment. The metadata is a JSON object that can be used to store any information you want about the comment, in `string`, `number`, or `boolean` form. Set a property to `null` to remove it. Corresponds to [`liveblocks.editCommentMetadata`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-metadata).\n\n`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.",
+ "operationId": "edit-comment-metadata",
"parameters": [
{
"name": "roomId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the room"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
+ }
},
{
"name": "threadId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the thread"
+ "type": "string",
+ "description": "ID of the thread",
+ "example": "th_abc123"
+ }
},
{
"name": "commentId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the comment"
+ "type": "string",
+ "description": "ID of the comment",
+ "example": "cm_abc123"
+ }
}
],
"responses": {
@@ -2900,10 +2908,11 @@
}
},
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpdateCommentMetadata"
+ "$ref": "#/components/schemas/EditCommentMetadataRequestBody"
},
"examples": {
"example": {
@@ -2925,15 +2934,15 @@
"/authorize-user": {
"post": {
"summary": "Get access token with secret key",
- "tags": ["Authentication"],
- "operationId": "post-authorize-user",
+ "tags": ["Auth"],
+ "operationId": "authorize-user",
"responses": {
"200": {
"description": "Success. Returns an access token that can be used to enter one or more rooms.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/TokenResponse"
+ "$ref": "#/components/schemas/AuthorizeUserResponse"
},
"examples": {
"example": {
@@ -2950,10 +2959,11 @@
}
},
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/AuthorizeUserRequest"
+ "$ref": "#/components/schemas/AuthorizeUserRequestBody"
},
"examples": {
"example": {
@@ -2975,21 +2985,21 @@
}
}
},
- "description": "This endpoint lets your application server (your back end) obtain a token that one of its clients (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own application’s custom authentication endpoint. When making this request, you’ll have to use your secret key.\n\n**Important:** The difference with an [ID token](#post-identify-user) is that an access token holds all the permissions, and is the source of truth. With ID tokens, permissions are set in the Liveblocks back end (through REST API calls) and \"checked at the door\" every time they are used to enter a room.\n\n**Note:** When using the `@liveblocks/node` package, you can use [`Liveblocks.prepareSession`](/docs/api-reference/liveblocks-node#access-tokens) in your back end to build this request.\n\nYou can pass the property `userId` in the request’s body. This can be whatever internal identifier you use for your user accounts as long as it uniquely identifies an account. The property `userId` is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId` corresponds to one MAU.\n\nAdditionally, you can set custom metadata to the token, which will be publicly accessible by other clients through the `user.info` property. This is useful for storing static data like avatar images or the user’s display name.\n\nLastly, you’ll specify the exact permissions to give to the user using the `permissions` field. This is done in an object where the keys are room names, or room name patterns (ending in a `*`), and a list of permissions to assign the user for any room that matches that name exactly (or starts with the pattern’s prefix). For tips, see [Manage permissions with access tokens](/docs/authentication/access-token)."
+ "description": "This endpoint lets your application server (your back end) obtain a token that one of its clients (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own application’s custom authentication endpoint. When making this request, you’ll have to use your secret key.\n\n**Important:** The difference with an [ID token](#post-identify-user) is that an access token holds all the permissions, and is the source of truth. With ID tokens, permissions are set in the Liveblocks back end (through REST API calls) and \"checked at the door\" every time they are used to enter a room.\n\n**Note:** When using the `@liveblocks/node` package, you can use [`Liveblocks.prepareSession`](https://liveblocks.io/docs/api-reference/liveblocks-node#access-tokens) in your back end to build this request.\n\nYou can pass the property `userId` in the request’s body. This can be whatever internal identifier you use for your user accounts as long as it uniquely identifies an account. The property `userId` is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId` corresponds to one MAU.\n\nAdditionally, you can set custom metadata to the token, which will be publicly accessible by other clients through the `user.info` property. This is useful for storing static data like avatar images or the user’s display name.\n\nLastly, you’ll specify the exact permissions to give to the user using the `permissions` field. This is done in an object where the keys are room names, or room name patterns (ending in a `*`), and a list of permissions to assign the user for any room that matches that name exactly (or starts with the pattern’s prefix). For tips, see [Manage permissions with access tokens](https://liveblocks.io/docs/authentication/access-token)."
}
},
"/identify-user": {
"post": {
"summary": "Get ID token with secret key",
- "tags": ["Authentication"],
- "operationId": "post-identify-user",
+ "tags": ["Auth"],
+ "operationId": "identify-user",
"responses": {
"200": {
"description": "Success. Returns an ID token that can be used to enter one or more rooms.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/TokenResponse"
+ "$ref": "#/components/schemas/IdentifyUserResponse"
},
"examples": {
"example": {
@@ -3006,10 +3016,11 @@
}
},
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/IdentifyUserRequest"
+ "$ref": "#/components/schemas/IdentifyUserRequestBody"
},
"examples": {
"example": {
@@ -3027,246 +3038,116 @@
}
}
},
- "description": "This endpoint lets your application server (your back end) obtain a token that one of its clients (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own application’s custom authentication endpoint. When using this endpoint to obtain ID tokens, you should manage your permissions by assigning user and/or group permissions to rooms explicitly, see our [Manage permissions with ID tokens](/docs/authentication/id-token) section.\n\n**Important:** The difference with an [access token](#post-authorize-user) is that an ID token doesn’t hold any permissions itself. With ID tokens, permissions are set in the Liveblocks back end (through REST API calls) and \"checked at the door\" every time they are used to enter a room. With access tokens, all permissions are set in the token itself, and thus controlled from your back end entirely.\n\n**Note:** When using the `@liveblocks/node` package, you can use [`Liveblocks.identifyUser`](/docs/api-reference/liveblocks-node) in your back end to build this request.\n\nYou can pass the property `userId` in the request’s body. This can be whatever internal identifier you use for your user accounts as long as it uniquely identifies an account. The property `userId` is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId` corresponds to one MAU.\n\nIf you want to use group permissions, you can also declare which `groupIds` this user belongs to. The group ID values are yours, but they will have to match the group IDs you assign permissions to when assigning permissions to rooms, see [Manage permissions with ID tokens](/docs/authentication/id-token)).\n\nAdditionally, you can set custom metadata to the token, which will be publicly accessible by other clients through the `user.info` property. This is useful for storing static data like avatar images or the user’s display name."
+ "description": "This endpoint lets your application server (your back end) obtain a token that one of its clients (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own application’s custom authentication endpoint. When using this endpoint to obtain ID tokens, you should manage your permissions by assigning user and/or group permissions to rooms explicitly, see our [Manage permissions with ID tokens](https://liveblocks.io/docs/authentication/id-token) section.\n\n**Important:** The difference with an [access token](#post-authorize-user) is that an ID token doesn’t hold any permissions itself. With ID tokens, permissions are set in the Liveblocks back end (through REST API calls) and \"checked at the door\" every time they are used to enter a room. With access tokens, all permissions are set in the token itself, and thus controlled from your back end entirely.\n\n**Note:** When using the `@liveblocks/node` package, you can use [`Liveblocks.identifyUser`](https://liveblocks.io/docs/api-reference/liveblocks-node) in your back end to build this request.\n\nYou can pass the property `userId` in the request’s body. This can be whatever internal identifier you use for your user accounts as long as it uniquely identifies an account. The property `userId` is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId` corresponds to one MAU.\n\nIf you want to use group permissions, you can also declare which `groupIds` this user belongs to. The group ID values are yours, but they will have to match the group IDs you assign permissions to when assigning permissions to rooms, see [Manage permissions with ID tokens](https://liveblocks.io/docs/authentication/id-token)).\n\nAdditionally, you can set custom metadata to the token, which will be publicly accessible by other clients through the `user.info` property. This is useful for storing static data like avatar images or the user’s display name."
}
},
- "/rooms/{roomId}/authorize": {
- "post": {
- "summary": "Get single-room token with secret key",
- "x-badge": "Deprecated",
- "description": "**Deprecated.** Prefer using [access tokens](#post-authorize-user) or [ID tokens](#post-identify-user) instead. Read more in our [migration guide](/docs/platform/upgrading/1.2).\n\nThis endpoint lets your application server (your back end) obtain a token that one of its clients (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own application’s custom authentication endpoint. When making this request, you’ll have to use your secret key.\n\nYou can pass the property `userId` in the request’s body. This can be whatever internal identifier you use for your user accounts as long as it uniquely identifies an account. Setting the `userId` is optional if you want to use public rooms, but it is required to enter a private room (because permissions are assigned to specific user IDs). In case you want to use the group permission system, you can also declare which `groupIds` this user belongs to.\n\nThe property userId is used by Liveblocks to calculate your account’s Monthly Active Users. One unique userId corresponds to one MAU. If you don’t pass a userId, we will create for you a new anonymous userId on each connection, but your MAUs will be higher.\n\nAdditionally, you can set custom metadata to the token, which will be publicly accessible by other clients through the `user.info` property. This is useful for storing static data like avatar images or the user’s display name.",
- "tags": ["Deprecated"],
+ "/users/{userId}/inbox-notifications/{inboxNotificationId}": {
+ "parameters": [
+ {
+ "schema": {
+ "type": "string",
+ "example": "user-123"
+ },
+ "name": "userId",
+ "in": "path",
+ "required": true
+ },
+ {
+ "schema": {
+ "type": "string",
+ "example": "in_abc123"
+ },
+ "name": "inboxNotificationId",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "get": {
+ "summary": "Get inbox notification",
+ "tags": ["Notifications"],
"parameters": [
{
+ "name": "userId",
+ "in": "path",
+ "required": true,
"schema": {
- "type": "string"
- },
- "name": "roomId",
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
+ }
+ },
+ {
+ "name": "inboxNotificationId",
"in": "path",
"required": true,
- "description": "ID of the room"
+ "schema": {
+ "type": "string",
+ "description": "ID of the inbox notification",
+ "example": "in_abc123"
+ }
}
],
- "operationId": "post-authorize",
+ "operationId": "get-inbox-notification",
+ "description": "This endpoint returns a user’s inbox notification by its ID. Corresponds to [`liveblocks.getInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications-inboxNotificationId).",
"responses": {
"200": {
- "description": "Success. Returns an old-style single-room token.",
+ "description": "Success. Returns the inbox notification.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Authorization"
- },
- "examples": {
- "example": {
- "value": {
- "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/InboxNotificationThreadData"
+ },
+ {
+ "$ref": "#/components/schemas/InboxNotificationCustomData"
}
- }
+ ]
}
}
}
},
"401": {
- "$ref": "#/components/responses/401"
+ "description": "Unauthorized"
},
"403": {
- "$ref": "#/components/responses/403"
+ "description": "Forbidden"
},
"404": {
- "$ref": "#/components/responses/404"
- },
- "422": {
- "$ref": "#/components/responses/422"
- }
- },
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/CreateAuthorization"
- },
- "examples": {
- "example": {
- "value": {
- "userId": "b2c1c290-f2c9-45de-a74e-6b7aa0690f59",
- "groupIds": ["g1", "g2"],
- "userInfo": {
- "name": "bob",
- "colors": ["blue", "red"]
- }
- }
- }
- }
- }
+ "description": "Not Found"
}
}
- }
- },
- "/rooms/{roomId}/public/authorize": {
- "post": {
- "summary": "Get single-room token with public key",
- "x-badge": "Deprecated",
- "tags": ["Deprecated"],
- "operationId": "post-public-authorize",
+ },
+ "delete": {
+ "summary": "Delete inbox notification",
+ "tags": ["Notifications"],
"parameters": [
{
- "schema": {
- "type": "string"
- },
- "name": "roomId",
- "in": "path",
- "required": true,
- "description": "ID of the room"
- }
- ],
- "responses": {
- "200": {
- "description": "Success. Returns the JWT token.",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Authorization"
- },
- "examples": {
- "example": {
- "value": {
- "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
- }
- }
- }
- }
- }
- },
- "403": {
- "$ref": "#/components/responses/403"
- },
- "404": {
- "$ref": "#/components/responses/404"
- },
- "422": {
- "$ref": "#/components/responses/422"
- }
- },
- "description": "**Deprecated.** When you update Liveblocks to 1.2, you no longer need to get a JWT token when using a public key.\n\nThis endpoint works with the public key and can be used client side. That means you don’t need to implement a dedicated authorization endpoint server side. \nThe generated JWT token works only with public room (`defaultAccesses: [\"room:write\"]`).",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/PublicAuthorizeBodyRequest"
- },
- "examples": {
- "example": {
- "value": {
- "publicApiKey": "pk_test_lOMrmwejSWLaPYQc5_JuGHXXX"
- }
- }
- }
- }
- }
- }
- }
- },
- "/users/{userId}/inbox-notifications/{inboxNotificationId}": {
- "parameters": [
- {
- "schema": {
- "type": "string"
- },
- "name": "userId",
- "in": "path",
- "required": true
- },
- {
- "schema": {
- "type": "string"
- },
- "name": "inboxNotificationId",
- "in": "path",
- "required": true
- }
- ],
- "get": {
- "summary": "Get inbox notification",
- "tags": ["Notifications"],
- "parameters": [
- {
- "name": "userId",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string"
- },
- "description": "ID of the user"
- },
- {
- "name": "inboxNotificationId",
+ "name": "userId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the inbox notification"
- }
- ],
- "operationId": "get-users-userId-inboxNotifications-inboxNotificationId",
- "description": "This endpoint returns a user’s inbox notification by its ID. Corresponds to [`liveblocks.getInboxNotification`](/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications-inboxNotificationId).",
- "responses": {
- "200": {
- "description": "",
- "content": {
- "application/json": {
- "schema": {
- "oneOf": [
- {
- "$ref": "#/components/schemas/InboxNotificationThreadData"
- },
- {
- "$ref": "#/components/schemas/InboxNotificationCustomData"
- }
- ]
- }
- }
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
}
},
- "401": {
- "description": "Unauthorized"
- },
- "403": {
- "description": "Forbidden"
- },
- "404": {
- "description": "Not Found"
- }
- }
- },
- "delete": {
- "summary": "Delete inbox notification",
- "tags": ["Notifications"],
- "parameters": [
- {
- "name": "userId",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string"
- },
- "description": "ID of the user"
- },
{
"name": "inboxNotificationId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the inbox notification"
+ "type": "string",
+ "description": "ID of the inbox notification",
+ "example": "in_abc123"
+ }
}
],
- "operationId": "delete-users-userId-inboxNotifications-inboxNotificationId",
- "description": "This endpoint deletes a user’s inbox notification by its ID.",
+ "operationId": "delete-inbox-notification",
+ "description": "This endpoint deletes a user’s inbox notification by its ID. Corresponds to [`liveblocks.deleteInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-users-userId-inbox-notifications-inboxNotificationId).",
"responses": {
"204": {
- "description": ""
+ "description": "Success. The inbox notification has been deleted."
},
"401": {
"description": "Unauthorized"
@@ -3284,7 +3165,8 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
@@ -3300,16 +3182,17 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the user"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
+ }
}
],
- "operationId": "delete-users-userId-inboxNotifications",
- "description": "This endpoint deletes all the user’s inbox notifications.",
+ "operationId": "delete-all-inbox-notifications",
+ "description": "This endpoint deletes all the user’s inbox notifications. Corresponds to [`liveblocks.deleteAllInboxNotifications`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-users-userId-inbox-notifications).",
"responses": {
"204": {
- "description": ""
+ "description": "Success. All inbox notifications for the user have been deleted."
},
"401": {
"description": "Unauthorized"
@@ -3331,65 +3214,60 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the user"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
+ }
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "The organization ID to filter notifications for.",
+ "example": "org_123456789"
},
"in": "query",
- "name": "organizationId",
- "description": "The organization ID to filter notifications for."
+ "name": "organizationId"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "Query to filter notifications. You can filter by `unread`, for example, `unread:true`.",
+ "example": "metadata[\"color\"]:\"blue\""
},
"in": "query",
- "name": "query",
- "description": "Query to filter notifications. You can filter by `unread`, for example, `unread:true`."
+ "name": "query"
},
{
"schema": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 50,
- "default": 50
+ "default": 50,
+ "description": "A limit on the number of inbox notifications to be returned. The limit can range between 1 and 50, and defaults to 50.",
+ "example": 20
},
"in": "query",
- "name": "limit",
- "description": "A limit on the number of inbox notifications to be returned. The limit can range between 1 and 50, and defaults to 50."
+ "name": "limit"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.",
+ "example": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9"
},
"in": "query",
- "name": "startingAfter",
- "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page."
+ "name": "startingAfter"
}
],
- "operationId": "get-users-userId-inboxNotifications",
- "description": "This endpoint returns all the user’s inbox notifications. Corresponds to [`liveblocks.getInboxNotifications`](/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications).",
+ "operationId": "get-inbox-notifications",
+ "description": "This endpoint returns all the user’s inbox notifications. Corresponds to [`liveblocks.getInboxNotifications`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications).",
"responses": {
"200": {
- "description": "",
+ "description": "Success. Returns the list of inbox notifications for the user.",
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "oneOf": [
- {
- "$ref": "#/components/schemas/InboxNotificationThreadData"
- },
- {
- "$ref": "#/components/schemas/InboxNotificationCustomData"
- }
- ]
- }
+ "$ref": "#/components/schemas/GetInboxNotificationsResponse"
}
}
}
@@ -3407,7 +3285,8 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
@@ -3417,22 +3296,23 @@
"get": {
"summary": "Get notification settings",
"tags": ["Notifications"],
- "operationId": "get-users-userId-notification-settings",
- "description": "This endpoint returns a user's notification settings for the project. Corresponds to [`liveblocks.getNotificationSettings`](/docs/api-reference/liveblocks-node#get-users-userId-notification-settings).",
+ "operationId": "get-notification-settings",
+ "description": "This endpoint returns a user's notification settings for the project. Corresponds to [`liveblocks.getNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-notification-settings).",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
- "required": true,
- "description": "ID of the user"
+ "required": true
}
],
"responses": {
"200": {
- "description": "",
+ "description": "Success. Returns the user's notification settings for the project.",
"content": {
"application/json": {
"schema": {
@@ -3478,24 +3358,26 @@
"post": {
"summary": "Update notification settings",
"tags": ["Notifications"],
- "operationId": "post-users-userId-notification-settings",
- "description": "This endpoint updates a user's notification settings for the project. Corresponds to [`liveblocks.updateNotificationSettings`](/docs/api-reference/liveblocks-node#post-users-userId-notification-settings).",
+ "operationId": "update-notification-settings",
+ "description": "This endpoint updates a user's notification settings for the project. Corresponds to [`liveblocks.updateNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-users-userId-notification-settings).",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
- "required": true,
- "description": "ID of the user"
+ "required": true
}
],
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/PartialNotificationSettings"
+ "$ref": "#/components/schemas/UpdateNotificationSettingsRequestBody"
},
"examples": {
"example": {
@@ -3518,7 +3400,7 @@
},
"responses": {
"200": {
- "description": "",
+ "description": "Success. Returns the updated notification settings.",
"content": {
"application/json": {
"schema": {
@@ -3567,17 +3449,18 @@
"delete": {
"summary": "Delete notification settings",
"tags": ["Notifications"],
- "operationId": "delete-users-userId-notification-settings",
- "description": "This endpoint deletes a user's notification settings for the project. Corresponds to [`liveblocks.deleteNotificationSettings`](/docs/api-reference/liveblocks-node#delete-users-userId-notification-settings).",
+ "operationId": "delete-notification-settings",
+ "description": "This endpoint deletes a user's notification settings for the project. Corresponds to [`liveblocks.deleteNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-users-userId-notification-settings).",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
- "required": true,
- "description": "ID of the user"
+ "required": true
}
],
"responses": {
@@ -3597,7 +3480,8 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
@@ -3605,7 +3489,8 @@
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
@@ -3615,31 +3500,33 @@
"get": {
"summary": "Get room subscription settings",
"tags": ["Notifications"],
- "operationId": "get-rooms-roomId-users-userId-subscription-settings",
- "description": "This endpoint returns a user’s subscription settings for a specific room. Corresponds to [`liveblocks.getRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#get-rooms-roomId-users-userId-subscription-settings).",
+ "operationId": "get-room-subscription-settings",
+ "description": "This endpoint returns a user’s subscription settings for a specific room. Corresponds to [`liveblocks.getRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-users-userId-subscription-settings).",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
- "required": true,
- "description": "ID of the user"
+ "required": true
}
],
"responses": {
"200": {
- "description": "",
+ "description": "Success. Returns the user's subscription settings for the specified room.",
"content": {
"application/json": {
"schema": {
@@ -3662,31 +3549,33 @@
"post": {
"summary": "Update room subscription settings",
"tags": ["Notifications"],
- "operationId": "post-rooms-roomId-users-userId-subscription-settings",
- "description": "This endpoint updates a user’s subscription settings for a specific room. Corresponds to [`liveblocks.updateRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#post-rooms-roomId-users-userId-subscription-settings).",
+ "operationId": "update-room-subscription-settings",
+ "description": "This endpoint updates a user’s subscription settings for a specific room. Corresponds to [`liveblocks.updateRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-users-userId-subscription-settings).",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
- "required": true,
- "description": "ID of the user"
+ "required": true
}
],
"responses": {
"200": {
- "description": "",
+ "description": "Success. Returns the updated room subscription settings.",
"content": {
"application/json": {
"schema": {
@@ -3709,10 +3598,11 @@
}
},
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/RoomSubscriptionSettings"
+ "$ref": "#/components/schemas/UpdateRoomSubscriptionSettingsRequestBody"
}
}
}
@@ -3721,26 +3611,28 @@
"delete": {
"summary": "Delete room subscription settings",
"tags": ["Notifications"],
- "operationId": "delete-rooms-roomId-users-userId-subscription-settings",
- "description": "This endpoint deletes a user’s subscription settings for a specific room. Corresponds to [`liveblocks.deleteRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#delete-rooms-roomId-users-userId-subscription-settings).",
+ "operationId": "delete-room-subscription-settings",
+ "description": "This endpoint deletes a user’s subscription settings for a specific room. Corresponds to [`liveblocks.deleteRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-users-userId-subscription-settings).",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
- "required": true,
- "description": "ID of the user"
+ "required": true
}
],
"responses": {
@@ -3763,7 +3655,8 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
@@ -3773,61 +3666,57 @@
"get": {
"summary": "Get user room subscription settings",
"tags": ["Notifications"],
- "operationId": "get-users-userId-subscription-settings",
- "description": "This endpoint returns the list of a user's room subscription settings. Corresponds to [`liveblocks.getUserRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#get-users-userId-room-subscription-settings).",
+ "operationId": "get-user-room-subscription-settings",
+ "description": "This endpoint returns the list of a user's room subscription settings. Corresponds to [`liveblocks.getUserRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-room-subscription-settings).",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
- "required": true,
- "description": "ID of the user"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.",
+ "example": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9"
},
"in": "query",
- "name": "startingAfter",
- "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page."
+ "name": "startingAfter"
},
{
"schema": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 50,
- "default": 50
+ "default": 50,
+ "description": "A limit on the number of elements to be returned. The limit can range between 1 and 50, and defaults to 50.",
+ "example": 20
+ },
+ "in": "query",
+ "name": "limit"
+ },
+ {
+ "schema": {
+ "type": "string",
+ "description": "The organization ID to filter room subscription settings for.",
+ "example": "org_123456789"
},
"in": "query",
- "name": "limit",
- "description": "A limit on the number of elements to be returned. The limit can range between 1 and 50, and defaults to 50."
+ "name": "organizationId"
}
],
"responses": {
"200": {
- "description": "",
+ "description": "Success. Returns the list of the user's room subscription settings.",
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/UserRoomSubscriptionSettings"
- }
- },
- "meta": {
- "type": "object",
- "properties": {
- "nextCursor": {
- "type": "string"
- }
- }
- }
- }
+ "$ref": "#/components/schemas/GetRoomSubscriptionSettingsResponse"
}
}
}
@@ -3848,7 +3737,8 @@
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
@@ -3856,7 +3746,8 @@
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
@@ -3865,33 +3756,36 @@
],
"get": {
"summary": "Get room notification settings",
- "operationId": "get-rooms-roomId-users-userId-notification-settings",
- "tags": ["Deprecated"],
+ "operationId": "get-room-notification-settings",
+ "tags": ["Notifications"],
+ "deprecated": true,
"x-badge": "Deprecated",
- "description": "**Deprecated.** Renamed to [`/subscription-settings`](get-rooms-roomId-users-userId-subscription-settings). Read more in our [migration guide](/docs/platform/upgrading/2.24).\n\nThis endpoint returns a user’s subscription settings for a specific room. Corresponds to [`liveblocks.getRoomNotificationSettings`](/docs/api-reference/liveblocks-node#get-rooms-roomId-users-userId-notification-settings).",
+ "description": "**Deprecated.** Renamed to [`/subscription-settings`](get-room-subscription-settings). Read more in our [migration guide](https://liveblocks.io/docs/platform/upgrading/2.24).\n\nThis endpoint returns a user’s subscription settings for a specific room. Corresponds to [`liveblocks.getRoomNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-users-userId-notification-settings).",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
- "required": true,
- "description": "ID of the user"
+ "required": true
}
],
"responses": {
"200": {
- "description": "",
+ "description": "Success. Returns the user's notification settings for the specified room.",
"content": {
"application/json": {
"schema": {
@@ -3913,33 +3807,36 @@
},
"post": {
"summary": "Update room notification settings",
- "operationId": "post-rooms-roomId-users-userId-notification-settings",
- "tags": ["Deprecated"],
+ "operationId": "update-room-notification-settings",
+ "tags": ["Notifications"],
+ "deprecated": true,
"x-badge": "Deprecated",
- "description": "**Deprecated.** Renamed to [`/subscription-settings`](post-rooms-roomId-users-userId-subscription-settings). Read more in our [migration guide](/docs/platform/upgrading/2.24).\n\nThis endpoint updates a user’s notification settings for a specific room. Corresponds to [`liveblocks.updateRoomNotificationSettings`](/docs/api-reference/liveblocks-node#post-rooms-roomId-users-userId-notification-settings).",
+ "description": "**Deprecated.** Renamed to [`/subscription-settings`](update-room-subscription-settings). Read more in our [migration guide](https://liveblocks.io/docs/platform/upgrading/2.24).\n\nThis endpoint updates a user’s notification settings for a specific room. Corresponds to [`liveblocks.updateRoomNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-users-userId-notification-settings).",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
- "required": true,
- "description": "ID of the user"
+ "required": true
}
],
"responses": {
"200": {
- "description": "",
+ "description": "Success. Returns the updated room notification settings.",
"content": {
"application/json": {
"schema": {
@@ -3965,7 +3862,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/RoomSubscriptionSettings"
+ "$ref": "#/components/schemas/UpdateRoomSubscriptionSettingsRequestBody"
}
}
}
@@ -3973,28 +3870,31 @@
},
"delete": {
"summary": "Delete room notification settings",
- "operationId": "delete-rooms-roomId-users-userId-notification-settings",
- "tags": ["Deprecated"],
+ "operationId": "delete-room-notification-settings",
+ "tags": ["Notifications"],
+ "deprecated": true,
"x-badge": "Deprecated",
- "description": "**Deprecated.** Renamed to [`/subscription-settings`](delete-rooms-roomId-users-userId-subscription-settings). Read more in our [migration guide](/docs/platform/upgrading/2.24).\n\nThis endpoint deletes a user’s notification settings for a specific room. Corresponds to [`liveblocks.deleteRoomNotificationSettings`](/docs/api-reference/liveblocks-node#delete-rooms-roomId-users-userId-notification-settings).",
+ "description": "**Deprecated.** Renamed to [`/subscription-settings`](delete-room-subscription-settings). Read more in our [migration guide](https://liveblocks.io/docs/platform/upgrading/2.24).\n\nThis endpoint deletes a user’s notification settings for a specific room. Corresponds to [`liveblocks.deleteRoomNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-users-userId-notification-settings).",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the room",
+ "example": "my-room-id"
},
"name": "roomId",
"in": "path",
- "required": true,
- "description": "ID of the room"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the user",
+ "example": "user-123"
},
"name": "userId",
"in": "path",
- "required": true,
- "description": "ID of the user"
+ "required": true
}
],
"responses": {
@@ -4018,8 +3918,8 @@
"post": {
"summary": "Trigger inbox notification",
"tags": ["Notifications"],
- "operationId": "post-inbox-notifications-trigger",
- "description": "This endpoint triggers an inbox notification. Corresponds to [`liveblocks.triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger).",
+ "operationId": "trigger-inbox-notification",
+ "description": "This endpoint triggers an inbox notification. Corresponds to [`liveblocks.triggerInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger).",
"responses": {
"200": {
"description": "No Content"
@@ -4041,7 +3941,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/TriggerInboxNotification"
+ "$ref": "#/components/schemas/TriggerInboxNotificationRequestBody"
}
}
}
@@ -4051,14 +3951,14 @@
"/groups": {
"post": {
"summary": "Create group",
- "description": "This endpoint creates a new group. Corresponds to [`liveblocks.createGroup`](/docs/api-reference/liveblocks-node#create-group).",
+ "description": "This endpoint creates a new group. Corresponds to [`liveblocks.createGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-group).",
"tags": ["Groups"],
- "operationId": "post-groups",
+ "operationId": "create-group",
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateGroup"
+ "$ref": "#/components/schemas/CreateGroupRequestBody"
}
}
}
@@ -4090,28 +3990,30 @@
},
"get": {
"summary": "Get groups",
- "description": "This endpoint returns a list of all groups in your project. Corresponds to [`liveblocks.getGroups`](/docs/api-reference/liveblocks-node#get-groups).",
+ "description": "This endpoint returns a list of all groups in your project. Corresponds to [`liveblocks.getGroups`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-groups).",
"tags": ["Groups"],
"operationId": "get-groups",
"parameters": [
{
"schema": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 100,
- "default": 20
+ "default": 20,
+ "description": "A limit on the number of groups to be returned. The limit can range between 1 and 100, and defaults to 20.",
+ "example": 20
},
"in": "query",
- "name": "limit",
- "description": "A limit on the number of groups to be returned. The limit can range between 1 and 100, and defaults to 20."
+ "name": "limit"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.",
+ "example": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9"
},
"in": "query",
- "name": "startingAfter",
- "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page."
+ "name": "startingAfter"
}
],
"responses": {
@@ -4120,7 +4022,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/GetGroups"
+ "$ref": "#/components/schemas/GetGroupsResponse"
}
}
}
@@ -4137,17 +4039,18 @@
"/groups/{groupId}": {
"get": {
"summary": "Get group",
- "description": "This endpoint returns a specific group by ID. Corresponds to [`liveblocks.getGroup`](/docs/api-reference/liveblocks-node#get-group).",
+ "description": "This endpoint returns a specific group by ID. Corresponds to [`liveblocks.getGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-group).",
"tags": ["Groups"],
- "operationId": "get-groups-groupId",
+ "operationId": "get-group",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "The ID of the group to retrieve.",
+ "example": "engineering"
},
"in": "path",
"name": "groupId",
- "description": "The ID of the group to retrieve.",
"required": true
}
],
@@ -4175,17 +4078,18 @@
},
"delete": {
"summary": "Delete group",
- "description": "This endpoint deletes a group. Corresponds to [`liveblocks.deleteGroup`](/docs/api-reference/liveblocks-node#delete-group).",
+ "description": "This endpoint deletes a group. Corresponds to [`liveblocks.deleteGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-group).",
"tags": ["Groups"],
- "operationId": "delete-groups-groupId",
+ "operationId": "delete-group",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "The ID of the group to delete.",
+ "example": "engineering"
},
"in": "path",
"name": "groupId",
- "description": "The ID of the group to delete.",
"required": true
}
],
@@ -4208,25 +4112,27 @@
"/groups/{groupId}/add-members": {
"post": {
"summary": "Add group members",
- "description": "This endpoint adds new members to an existing group. Corresponds to [`liveblocks.addGroupMembers`](/docs/api-reference/liveblocks-node#add-group-members).",
+ "description": "This endpoint adds new members to an existing group. Corresponds to [`liveblocks.addGroupMembers`](https://liveblocks.io/docs/api-reference/liveblocks-node#add-group-members).",
"tags": ["Groups"],
- "operationId": "post-groups-groupId-add-members",
+ "operationId": "add-group-members",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "The ID of the group to add members to.",
+ "example": "engineering"
},
"in": "path",
"name": "groupId",
- "description": "The ID of the group to add members to.",
"required": true
}
],
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/AddGroupMembers"
+ "$ref": "#/components/schemas/AddGroupMembersRequestBody"
}
}
}
@@ -4260,25 +4166,27 @@
"/groups/{groupId}/remove-members": {
"post": {
"summary": "Remove group members",
- "description": "This endpoint removes members from an existing group. Corresponds to [`liveblocks.removeGroupMembers`](/docs/api-reference/liveblocks-node#remove-group-members).",
+ "description": "This endpoint removes members from an existing group. Corresponds to [`liveblocks.removeGroupMembers`](https://liveblocks.io/docs/api-reference/liveblocks-node#remove-group-members).",
"tags": ["Groups"],
- "operationId": "post-groups-groupId-remove-members",
+ "operationId": "remove-group-members",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "The ID of the group to remove members from.",
+ "example": "engineering"
},
"in": "path",
"name": "groupId",
- "description": "The ID of the group to remove members from.",
"required": true
}
],
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/RemoveGroupMembers"
+ "$ref": "#/components/schemas/RemoveGroupMembersRequestBody"
}
}
}
@@ -4312,37 +4220,40 @@
"/users/{userId}/groups": {
"get": {
"summary": "Get user groups",
- "description": "This endpoint returns all groups that a specific user is a member of. Corresponds to [`liveblocks.getUserGroups`](/docs/api-reference/liveblocks-node#get-user-groups).",
+ "description": "This endpoint returns all groups that a specific user is a member of. Corresponds to [`liveblocks.getUserGroups`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-user-groups).",
"tags": ["Groups"],
- "operationId": "get-users-userId-groups",
+ "operationId": "get-user-groups",
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "The ID of the user to get groups for.",
+ "example": "user-123"
},
"in": "path",
"name": "userId",
- "description": "The ID of the user to get groups for.",
"required": true
},
{
"schema": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 100,
- "default": 20
+ "default": 20,
+ "description": "A limit on the number of groups to be returned. The limit can range between 1 and 100, and defaults to 20.",
+ "example": 20
},
"in": "query",
- "name": "limit",
- "description": "A limit on the number of groups to be returned. The limit can range between 1 and 100, and defaults to 20."
+ "name": "limit"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.",
+ "example": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9"
},
"in": "query",
- "name": "startingAfter",
- "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page."
+ "name": "startingAfter"
}
],
"responses": {
@@ -4351,7 +4262,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/GetUserGroups"
+ "$ref": "#/components/schemas/GetUserGroupsResponse"
}
}
}
@@ -4371,27 +4282,29 @@
"/ai/copilots": {
"get": {
"summary": "Get AI copilots",
- "description": "This endpoint returns a paginated list of AI copilots. The copilots are returned sorted by creation date, from newest to oldest. Corresponds to [`liveblocks.getAiCopilots`](/docs/api-reference/liveblocks-node#get-ai-copilots).",
+ "description": "This endpoint returns a paginated list of AI copilots. The copilots are returned sorted by creation date, from newest to oldest. Corresponds to [`liveblocks.getAiCopilots`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-ai-copilots).",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 100,
- "default": 20
+ "default": 20,
+ "description": "A limit on the number of copilots to be returned. The limit can range between 1 and 100, and defaults to 20.",
+ "example": 20
},
"in": "query",
- "name": "limit",
- "description": "A limit on the number of copilots to be returned. The limit can range between 1 and 100, and defaults to 20."
+ "name": "limit"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.",
+ "example": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9"
},
"in": "query",
- "name": "startingAfter",
- "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page."
+ "name": "startingAfter"
}
],
"responses": {
@@ -4400,7 +4313,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/GetAiCopilots"
+ "$ref": "#/components/schemas/GetAiCopilotsResponse"
}
}
}
@@ -4416,7 +4329,7 @@
},
"post": {
"summary": "Create AI copilot",
- "description": "This endpoint creates a new AI copilot with the given configuration. Corresponds to [`liveblocks.createAiCopilot`](/docs/api-reference/liveblocks-node#create-ai-copilot).",
+ "description": "This endpoint creates a new AI copilot with the given configuration. Corresponds to [`liveblocks.createAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-ai-copilot).",
"tags": ["AI"],
"responses": {
"201": {
@@ -4441,10 +4354,11 @@
},
"operationId": "create-ai-copilot",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateAiCopilot"
+ "$ref": "#/components/schemas/CreateAiCopilotRequestBody"
}
}
}
@@ -4454,17 +4368,18 @@
"/ai/copilots/{copilotId}": {
"get": {
"summary": "Get AI copilot",
- "description": "This endpoint returns an AI copilot by its ID. Corresponds to [`liveblocks.getAiCopilot`](/docs/api-reference/liveblocks-node#get-ai-copilot).",
+ "description": "This endpoint returns an AI copilot by its ID. Corresponds to [`liveblocks.getAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-ai-copilot).",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the AI copilot",
+ "example": "cp_abc123"
},
"name": "copilotId",
"in": "path",
- "required": true,
- "description": "ID of the AI copilot"
+ "required": true
}
],
"responses": {
@@ -4492,17 +4407,18 @@
},
"post": {
"summary": "Update AI copilot",
- "description": "This endpoint updates an existing AI copilot's configuration. Corresponds to [`liveblocks.updateAiCopilot`](/docs/api-reference/liveblocks-node#update-ai-copilot).\n\nThis endpoint returns a 422 response if the update doesn't apply due to validation failures. For example, if the existing copilot uses the \"openai\" provider and you attempt to update the provider model to an incompatible value for the provider, like \"gemini-2.5-pro\", you'll receive a 422 response with an error message explaining where the validation failed.",
+ "description": "This endpoint updates an existing AI copilot's configuration. Corresponds to [`liveblocks.updateAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#update-ai-copilot).\n\nThis endpoint returns a 422 response if the update doesn't apply due to validation failures. For example, if the existing copilot uses the \"openai\" provider and you attempt to update the provider model to an incompatible value for the provider, like \"gemini-2.5-pro\", you'll receive a 422 response with an error message explaining where the validation failed.",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the AI copilot",
+ "example": "cp_abc123"
},
"name": "copilotId",
"in": "path",
- "required": true,
- "description": "ID of the AI copilot"
+ "required": true
}
],
"responses": {
@@ -4531,10 +4447,11 @@
},
"operationId": "update-ai-copilot",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpdateAiCopilot"
+ "$ref": "#/components/schemas/UpdateAiCopilotRequestBody"
}
}
}
@@ -4542,17 +4459,18 @@
},
"delete": {
"summary": "Delete AI copilot",
- "description": "This endpoint deletes an AI copilot by its ID. A deleted copilot is no longer accessible and cannot be restored. Corresponds to [`liveblocks.deleteAiCopilot`](/docs/api-reference/liveblocks-node#delete-ai-copilot).",
+ "description": "This endpoint deletes an AI copilot by its ID. A deleted copilot is no longer accessible and cannot be restored. Corresponds to [`liveblocks.deleteAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-ai-copilot).",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the AI copilot",
+ "example": "cp_abc123"
},
"name": "copilotId",
"in": "path",
- "required": true,
- "description": "ID of the AI copilot"
+ "required": true
}
],
"responses": {
@@ -4575,36 +4493,39 @@
"/ai/copilots/{copilotId}/knowledge": {
"get": {
"summary": "Get knowledge sources",
- "description": "This endpoint returns a paginated list of knowledge sources for a specific AI copilot. Corresponds to [`liveblocks.getKnowledgeSources`](/docs/api-reference/liveblocks-node#get-knowledge-sources).",
+ "description": "This endpoint returns a paginated list of knowledge sources for a specific AI copilot. Corresponds to [`liveblocks.getKnowledgeSources`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-knowledge-sources).",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the AI copilot",
+ "example": "cp_abc123"
},
"name": "copilotId",
"in": "path",
- "required": true,
- "description": "ID of the AI copilot"
+ "required": true
},
{
"schema": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 100,
- "default": 20
+ "default": 20,
+ "description": "A limit on the number of knowledge sources to be returned. The limit can range between 1 and 100, and defaults to 20.",
+ "example": 20
},
"in": "query",
- "name": "limit",
- "description": "A limit on the number of knowledge sources to be returned. The limit can range between 1 and 100, and defaults to 20."
+ "name": "limit"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.",
+ "example": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9"
},
"in": "query",
- "name": "startingAfter",
- "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page."
+ "name": "startingAfter"
}
],
"responses": {
@@ -4613,7 +4534,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/GetKnowledgeSources"
+ "$ref": "#/components/schemas/GetKnowledgeSourcesResponse"
}
}
}
@@ -4634,26 +4555,28 @@
"/ai/copilots/{copilotId}/knowledge/{knowledgeSourceId}": {
"get": {
"summary": "Get knowledge source",
- "description": "This endpoint returns a specific knowledge source by its ID. Corresponds to [`liveblocks.getKnowledgeSource`](/docs/api-reference/liveblocks-node#get-knowledge-source).",
+ "description": "This endpoint returns a specific knowledge source by its ID. Corresponds to [`liveblocks.getKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-knowledge-source).",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the AI copilot",
+ "example": "cp_abc123"
},
"name": "copilotId",
"in": "path",
- "required": true,
- "description": "ID of the AI copilot"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the knowledge source",
+ "example": "ks_abc123"
},
"name": "knowledgeSourceId",
"in": "path",
- "required": true,
- "description": "ID of the knowledge source"
+ "required": true
}
],
"responses": {
@@ -4683,17 +4606,18 @@
"/ai/copilots/{copilotId}/knowledge/web": {
"post": {
"summary": "Create web knowledge source",
- "description": "This endpoint creates a web knowledge source for an AI copilot. This allows the copilot to access and learn from web content. Corresponds to [`liveblocks.createWebKnowledgeSource`](/docs/api-reference/liveblocks-node#create-web-knowledge-source).",
+ "description": "This endpoint creates a web knowledge source for an AI copilot. This allows the copilot to access and learn from web content. Corresponds to [`liveblocks.createWebKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-web-knowledge-source).",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the AI copilot",
+ "example": "cp_abc123"
},
"name": "copilotId",
"in": "path",
- "required": true,
- "description": "ID of the AI copilot"
+ "required": true
}
],
"responses": {
@@ -4702,12 +4626,7 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- }
- }
+ "$ref": "#/components/schemas/CreateWebKnowledgeSourceResponse"
}
}
}
@@ -4724,10 +4643,11 @@
},
"operationId": "create-web-knowledge-source",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateWebKnowledgeSource"
+ "$ref": "#/components/schemas/CreateWebKnowledgeSourceRequestBody"
}
}
}
@@ -4737,26 +4657,28 @@
"/ai/copilots/{copilotId}/knowledge/file/{name}": {
"put": {
"summary": "Create file knowledge source",
- "description": "This endpoint creates a file knowledge source for an AI copilot by uploading a file. The copilot can then reference the content of the file when responding. Corresponds to [`liveblocks.createFileKnowledgeSource`](/docs/api-reference/liveblocks-node#create-file-knowledge-source).",
+ "description": "This endpoint creates a file knowledge source for an AI copilot by uploading a file. The copilot can then reference the content of the file when responding. Corresponds to [`liveblocks.createFileKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-file-knowledge-source).",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the AI copilot",
+ "example": "cp_abc123"
},
"name": "copilotId",
"in": "path",
- "required": true,
- "description": "ID of the AI copilot"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "Name of the file",
+ "example": "document.pdf"
},
"name": "name",
"in": "path",
- "required": true,
- "description": "Name of the file"
+ "required": true
}
],
"responses": {
@@ -4787,6 +4709,7 @@
},
"operationId": "create-file-knowledge-source",
"requestBody": {
+ "required": true,
"content": {
"application/octet-stream": {
"schema": {
@@ -4801,43 +4724,37 @@
"/ai/copilots/{copilotId}/knowledge/file/{knowledgeSourceId}": {
"get": {
"summary": "Get file knowledge source content",
- "description": "This endpoint returns the content of a file knowledge source as Markdown. This allows you to see what content the AI copilot has access to from uploaded files. Corresponds to [`liveblocks.getFileKnowledgeSourceMarkdown`](/docs/api-reference/liveblocks-node#get-file-knowledge-source-markdown).",
+ "description": "This endpoint returns the content of a file knowledge source as markdown. This allows you to see what content the AI copilot has access to from uploaded files. Corresponds to [`liveblocks.getFileKnowledgeSourceMarkdown`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-file-knowledge-source-markdown).",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the AI copilot",
+ "example": "cp_abc123"
},
"name": "copilotId",
"in": "path",
- "required": true,
- "description": "ID of the AI copilot"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the knowledge source",
+ "example": "ks_abc123"
},
"name": "knowledgeSourceId",
"in": "path",
- "required": true,
- "description": "ID of the knowledge source"
+ "required": true
}
],
"responses": {
"200": {
- "description": "Success. Returns the content of the file knowledge source.",
+ "description": "Success. Returns the content of the file knowledge source as markdown.",
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "content": {
- "type": "string"
- }
- }
+ "$ref": "#/components/schemas/GetFileKnowledgeSourceMarkdownResponse"
}
}
}
@@ -4852,30 +4769,32 @@
"$ref": "#/components/responses/404"
}
},
- "operationId": "get-file-knowledge-source-content"
+ "operationId": "get-file-knowledge-source-markdown"
},
"delete": {
"summary": "Delete file knowledge source",
- "description": "This endpoint deletes a file knowledge source from an AI copilot. The copilot will no longer have access to the content from this file. Corresponds to [`liveblocks.deleteFileKnowledgeSource`](/docs/api-reference/liveblocks-node#delete-file-knowledge-source).",
+ "description": "This endpoint deletes a file knowledge source from an AI copilot. The copilot will no longer have access to the content from this file. Corresponds to [`liveblocks.deleteFileKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-file-knowledge-source).",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the AI copilot",
+ "example": "cp_abc123"
},
"name": "copilotId",
"in": "path",
- "required": true,
- "description": "ID of the AI copilot"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the knowledge source",
+ "example": "ks_abc123"
},
"name": "knowledgeSourceId",
"in": "path",
- "required": true,
- "description": "ID of the knowledge source"
+ "required": true
}
],
"responses": {
@@ -4898,26 +4817,28 @@
"/ai/copilots/{copilotId}/knowledge/web/{knowledgeSourceId}": {
"delete": {
"summary": "Delete web knowledge source",
- "description": "This endpoint deletes a web knowledge source from an AI copilot. The copilot will no longer have access to the content from this source. Corresponds to [`liveblocks.deleteWebKnowledgeSource`](/docs/api-reference/liveblocks-node#delete-web-knowledge-source).",
+ "description": "This endpoint deletes a web knowledge source from an AI copilot. The copilot will no longer have access to the content from this source. Corresponds to [`liveblocks.deleteWebKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-web-knowledge-source).",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the AI copilot",
+ "example": "cp_abc123"
},
"name": "copilotId",
"in": "path",
- "required": true,
- "description": "ID of the AI copilot"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the knowledge source",
+ "example": "ks_abc123"
},
"name": "knowledgeSourceId",
"in": "path",
- "required": true,
- "description": "ID of the knowledge source"
+ "required": true
}
],
"responses": {
@@ -4940,45 +4861,49 @@
"/ai/copilots/{copilotId}/knowledge/web/{knowledgeSourceId}/links": {
"get": {
"summary": "Get web knowledge source links",
- "description": "This endpoint returns a paginated list of links that were indexed from a web knowledge source. This is useful for understanding what content the AI copilot has access to from web sources. Corresponds to [`liveblocks.getWebKnowledgeSourceLinks`](/docs/api-reference/liveblocks-node#get-web-knowledge-source-links).",
+ "description": "This endpoint returns a paginated list of links that were indexed from a web knowledge source. This is useful for understanding what content the AI copilot has access to from web sources. Corresponds to [`liveblocks.getWebKnowledgeSourceLinks`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-web-knowledge-source-links).",
"tags": ["AI"],
"parameters": [
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the AI copilot",
+ "example": "cp_abc123"
},
"name": "copilotId",
"in": "path",
- "required": true,
- "description": "ID of the AI copilot"
+ "required": true
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "ID of the knowledge source",
+ "example": "ks_abc123"
},
"name": "knowledgeSourceId",
"in": "path",
- "required": true,
- "description": "ID of the knowledge source"
+ "required": true
},
{
"schema": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 100,
- "default": 20
+ "default": 20,
+ "description": "A limit on the number of links to be returned. The limit can range between 1 and 100, and defaults to 20.",
+ "example": 20
},
"in": "query",
- "name": "limit",
- "description": "A limit on the number of links to be returned. The limit can range between 1 and 100, and defaults to 20."
+ "name": "limit"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.",
+ "example": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9"
},
"in": "query",
- "name": "startingAfter",
- "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page."
+ "name": "startingAfter"
}
],
"responses": {
@@ -4987,7 +4912,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/GetWebKnowledgeSourceLinks"
+ "$ref": "#/components/schemas/GetWebKnowledgeSourceLinksResponse"
}
}
}
@@ -5014,22 +4939,24 @@
"parameters": [
{
"schema": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 100,
- "default": 20
+ "default": 20,
+ "description": "A limit on the number of projects to return. The limit can range between 1 and 100, and defaults to 20.",
+ "example": 20
},
"in": "query",
- "name": "limit",
- "description": "A limit on the number of projects to return. The limit can range between 1 and 100, and defaults to 20."
+ "name": "limit"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.",
+ "example": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9"
},
"in": "query",
- "name": "cursor",
- "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page."
+ "name": "cursor"
}
],
"responses": {
@@ -5038,7 +4965,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementProjectsResponse"
+ "$ref": "#/components/schemas/GetManagementProjectsResponse"
},
"examples": {
"example": {
@@ -5094,7 +5021,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementProjectResponse"
+ "$ref": "#/components/schemas/ManagementProject"
},
"examples": {
"example": {
@@ -5134,12 +5061,13 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-management-projects",
+ "operationId": "create-management-project",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateManagementProject"
+ "$ref": "#/components/schemas/CreateManagementProjectRequestBody"
},
"examples": {
"example": {
@@ -5167,9 +5095,10 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
}
],
"responses": {
@@ -5178,7 +5107,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementProjectResponse"
+ "$ref": "#/components/schemas/ManagementProject"
}
}
}
@@ -5209,9 +5138,10 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
}
],
"responses": {
@@ -5220,7 +5150,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementProjectResponse"
+ "$ref": "#/components/schemas/ManagementProject"
}
}
}
@@ -5238,12 +5168,13 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-management-project",
+ "operationId": "update-management-project",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpdateManagementProject"
+ "$ref": "#/components/schemas/UpdateManagementProjectRequestBody"
},
"examples": {
"example": {
@@ -5268,9 +5199,10 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
}
],
"responses": {
@@ -5302,9 +5234,10 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
}
],
"responses": {
@@ -5321,7 +5254,7 @@
"$ref": "#/components/responses/404"
}
},
- "operationId": "post-management-project-public-key-activate"
+ "operationId": "activate-project-public-api-key"
}
},
"/management/projects/{projectId}/api-keys/public/deactivate": {
@@ -5336,9 +5269,10 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
}
],
"responses": {
@@ -5355,7 +5289,7 @@
"$ref": "#/components/responses/404"
}
},
- "operationId": "post-management-project-public-key-deactivate"
+ "operationId": "deactivate-project-public-api-key"
}
},
"/management/projects/{projectId}/api-keys/public/roll": {
@@ -5370,9 +5304,10 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
}
],
"responses": {
@@ -5381,7 +5316,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementProjectPublicKeyResponse"
+ "$ref": "#/components/schemas/RollProjectPublicApiKeyResponse"
}
}
}
@@ -5399,13 +5334,13 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-management-project-public-key-roll",
+ "operationId": "roll-project-public-api-key",
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementProjectKeyRollRequest"
+ "$ref": "#/components/schemas/RollProjectPublicApiKeyRequestBody"
},
"examples": {
"example": {
@@ -5431,9 +5366,10 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
}
],
"responses": {
@@ -5442,7 +5378,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementProjectSecretKeyResponse"
+ "$ref": "#/components/schemas/RollProjectSecretApiKeyResponse"
}
}
}
@@ -5460,13 +5396,13 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-management-project-secret-key-roll",
+ "operationId": "roll-project-secret-api-key",
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementProjectKeyRollRequest"
+ "$ref": "#/components/schemas/RollProjectSecretApiKeyRequestBody"
},
"examples": {
"example": {
@@ -5492,28 +5428,31 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
},
{
"schema": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 100,
- "default": 20
+ "default": 20,
+ "description": "A limit on the number of webhooks to return. The limit can range between 1 and 100, and defaults to 20.",
+ "example": 20
},
"in": "query",
- "name": "limit",
- "description": "A limit on the number of webhooks to return. The limit can range between 1 and 100, and defaults to 20."
+ "name": "limit"
},
{
"schema": {
- "type": "string"
+ "type": "string",
+ "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.",
+ "example": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9"
},
"in": "query",
- "name": "cursor",
- "description": "A cursor used for pagination. Get the value from the `nextCursor` response of the previous page."
+ "name": "cursor"
}
],
"responses": {
@@ -5522,7 +5461,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhooksResponse"
+ "$ref": "#/components/schemas/GetManagementWebhooksResponse"
}
}
}
@@ -5553,9 +5492,10 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
}
],
"responses": {
@@ -5564,7 +5504,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookResponse"
+ "$ref": "#/components/schemas/ManagementWebhook"
}
}
}
@@ -5582,12 +5522,13 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-management-webhooks",
+ "operationId": "create-management-webhook",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateManagementWebhook"
+ "$ref": "#/components/schemas/CreateManagementWebhookRequestBody"
},
"examples": {
"example": {
@@ -5620,18 +5561,20 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
},
{
"name": "webhookId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the webhook"
+ "type": "string",
+ "description": "ID of the webhook",
+ "example": "wh_abc123"
+ }
}
],
"responses": {
@@ -5640,7 +5583,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookResponse"
+ "$ref": "#/components/schemas/ManagementWebhook"
}
}
}
@@ -5668,18 +5611,20 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
},
{
"name": "webhookId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the webhook"
+ "type": "string",
+ "description": "ID of the webhook",
+ "example": "wh_abc123"
+ }
}
],
"responses": {
@@ -5688,7 +5633,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookResponse"
+ "$ref": "#/components/schemas/ManagementWebhook"
}
}
}
@@ -5706,12 +5651,13 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-management-webhook",
+ "operationId": "update-management-webhook",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpdateManagementWebhook"
+ "$ref": "#/components/schemas/UpdateManagementWebhookRequestBody"
},
"examples": {
"example": {
@@ -5740,18 +5686,20 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
},
{
"name": "webhookId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the webhook"
+ "type": "string",
+ "description": "ID of the webhook",
+ "example": "wh_abc123"
+ }
}
],
"responses": {
@@ -5783,18 +5731,20 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
},
{
"name": "webhookId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the webhook"
+ "type": "string",
+ "description": "ID of the webhook",
+ "example": "wh_abc123"
+ }
}
],
"responses": {
@@ -5803,7 +5753,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookSecretRotateResponse"
+ "$ref": "#/components/schemas/RotateManagementWebhookSecretResponse"
}
}
}
@@ -5818,7 +5768,7 @@
"$ref": "#/components/responses/404"
}
},
- "operationId": "post-management-webhook-secret-roll"
+ "operationId": "roll-management-webhook-secret"
}
},
"/management/projects/{projectId}/webhooks/{webhookId}/additional-headers": {
@@ -5833,18 +5783,20 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
},
{
"name": "webhookId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the webhook"
+ "type": "string",
+ "description": "ID of the webhook",
+ "example": "wh_abc123"
+ }
}
],
"responses": {
@@ -5853,7 +5805,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookHeadersResponse"
+ "$ref": "#/components/schemas/UpsertManagementWebhookHeadersResponse"
}
}
}
@@ -5871,12 +5823,13 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-management-webhook-headers",
+ "operationId": "upsert-management-webhook-additional-headers",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookHeadersPatch"
+ "$ref": "#/components/schemas/UpsertManagementWebhookHeadersRequestBody"
},
"examples": {
"example": {
@@ -5902,18 +5855,20 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
},
{
"name": "webhookId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the webhook"
+ "type": "string",
+ "description": "ID of the webhook",
+ "example": "wh_abc123"
+ }
}
],
"responses": {
@@ -5922,7 +5877,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookHeadersResponse"
+ "$ref": "#/components/schemas/GetManagementWebhookHeadersResponse"
}
}
}
@@ -5937,7 +5892,7 @@
"$ref": "#/components/responses/404"
}
},
- "operationId": "get-management-webhook-headers"
+ "operationId": "get-management-webhook-additional-headers"
}
},
"/management/projects/{projectId}/webhooks/{webhookId}/delete-additional-headers": {
@@ -5952,18 +5907,20 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
},
{
"name": "webhookId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the webhook"
+ "type": "string",
+ "description": "ID of the webhook",
+ "example": "wh_abc123"
+ }
}
],
"responses": {
@@ -5972,7 +5929,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookHeadersResponse"
+ "$ref": "#/components/schemas/DeleteManagementWebhookHeadersResponse"
}
}
}
@@ -5990,12 +5947,13 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-management-webhook-headers-delete",
+ "operationId": "delete-management-webhook-additional-headers",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookHeadersDelete"
+ "$ref": "#/components/schemas/DeleteManagementWebhookHeadersRequestBody"
},
"examples": {
"example": {
@@ -6021,18 +5979,20 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
},
{
"name": "webhookId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the webhook"
+ "type": "string",
+ "description": "ID of the webhook",
+ "example": "wh_abc123"
+ }
}
],
"responses": {
@@ -6052,12 +6012,13 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-management-webhook-recover-failed-messages",
+ "operationId": "recover-failed-webhook-messages",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookRecoverRequest"
+ "$ref": "#/components/schemas/RecoverManagementWebhookFailedMessagesRequestBody"
},
"examples": {
"example": {
@@ -6083,18 +6044,20 @@
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the project"
+ "type": "string",
+ "description": "ID of the project",
+ "example": "683d49ed6b4d1cec5a597b13"
+ }
},
{
"name": "webhookId",
"in": "path",
"required": true,
"schema": {
- "type": "string"
- },
- "description": "ID of the webhook"
+ "type": "string",
+ "description": "ID of the webhook",
+ "example": "wh_abc123"
+ }
}
],
"responses": {
@@ -6103,7 +6066,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookTestResponse"
+ "$ref": "#/components/schemas/TestManagementWebhookResponse"
}
}
}
@@ -6121,12 +6084,13 @@
"$ref": "#/components/responses/422"
}
},
- "operationId": "post-management-webhook-test",
+ "operationId": "send-test-webhook",
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ManagementWebhookTestRequest"
+ "$ref": "#/components/schemas/TestManagementWebhookRequestBody"
},
"examples": {
"example": {
@@ -6154,6 +6118,10 @@
"type": "string",
"enum": ["room"]
},
+ "organizationId": {
+ "type": "string",
+ "description": "The organization ID associated with this room."
+ },
"lastConnectionAt": {
"type": "string",
"format": "date-time"
@@ -6162,288 +6130,343 @@
"type": "string",
"format": "date-time"
},
- "metadata": {
- "type": "object",
- "additionalProperties": {
- "type": "string"
- }
- },
"defaultAccesses": {
- "type": ["string", "array"],
- "uniqueItems": true,
- "items": {}
+ "$ref": "#/components/schemas/RoomPermission"
},
"usersAccesses": {
- "type": "object"
+ "$ref": "#/components/schemas/RoomAccesses"
},
"groupsAccesses": {
- "type": "object"
+ "$ref": "#/components/schemas/RoomAccesses"
+ },
+ "metadata": {
+ "$ref": "#/components/schemas/RoomMetadata"
+ },
+ "organizationId": {
+ "type": "string"
}
- }
- },
- "UpdateRoom": {
- "type": "object",
- "title": "UpdateRoom",
+ },
"additionalProperties": false,
- "properties": {
- "defaultAccesses": {
- "type": ["array", "null"],
- "items": {
- "type": "string"
- }
- },
- "usersAccesses": {
- "type": ["object", "null"],
- "description": "A map of user identifiers to permissions list. Setting the value as `null` will clear all users’ accesses. Setting one user identifier as `null` will clear this user’s accesses."
+ "required": [
+ "id",
+ "type",
+ "createdAt",
+ "defaultAccesses",
+ "usersAccesses",
+ "groupsAccesses",
+ "metadata",
+ "organizationId"
+ ],
+ "example": {
+ "type": "room",
+ "id": "my-room-id",
+ "lastConnectionAt": "2022-08-04T21:07:09.380Z",
+ "createdAt": "2022-07-13T14:32:50.697Z",
+ "organizationId": "org_123456789",
+ "metadata": {
+ "color": "blue",
+ "type": "whiteboard"
},
+ "defaultAccesses": ["room:write"],
"groupsAccesses": {
- "type": ["object", "null"],
- "description": "A map of group identifiers to permissions list. Setting the value as `null` will clear all groups’ accesses. Setting one group identifier as `null` will clear this group’s accesses."
+ "marketing": ["room:write"]
},
- "metadata": {
- "type": ["object", "null"],
- "description": "A map of metadata keys to their values (`string` or `string[]`). Setting the value as `null` will clear all metadata. Setting a key as `null` will clear the key."
+ "usersAccesses": {
+ "alice": ["room:write"]
}
}
},
- "UpsertRoom": {
+ "RoomMetadata": {
+ "title": "RoomMetadata",
"type": "object",
- "title": "UpsertRoom",
- "additionalProperties": false,
- "properties": {
- "update": {
- "type": "object",
- "properties": {
- "defaultAccesses": {
- "type": ["array", "null"],
- "items": {
- "type": "string"
- }
- },
- "usersAccesses": {
- "type": ["object", "null"],
- "description": "A map of user identifiers to permissions list. Setting the value as `null` will clear all users’ accesses. Setting one user identifier as `null` will clear this user’s accesses."
- },
- "groupsAccesses": {
- "type": ["object", "null"],
- "description": "A map of group identifiers to permissions list. Setting the value as `null` will clear all groups’ accesses. Setting one group identifier as `null` will clear this group’s accesses."
- },
- "metadata": {
- "type": ["object", "null"],
- "description": "A map of metadata keys to their values (`string` or `string[]`). Setting the value as `null` will clear all metadata. Setting a key as `null` will clear the key."
+ "additionalProperties": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
}
}
- },
- "create": {
- "type": "object",
- "properties": {
- "defaultAccesses": {
- "type": ["array"],
- "items": {
- "type": "string"
- }
+ ]
+ },
+ "example": {
+ "color": "blue",
+ "type": "whiteboard"
+ }
+ },
+ "RoomPermission": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "room:write",
+ "room:read",
+ "room:presence:write",
+ "comments:write"
+ ]
+ },
+ "uniqueItems": true,
+ "example": ["room:read", "room:presence:write"]
+ },
+ "RoomAccesses": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "room:write",
+ "room:read",
+ "room:presence:write",
+ "comments:write"
+ ]
+ }
+ },
+ "example": {
+ "alice": ["room:write"],
+ "bob": ["room:read", "room:presence:write"]
+ }
+ },
+ "GetRoomsResponse": {
+ "title": "GetRoomsResponse",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["nextCursor", "data"],
+ "properties": {
+ "nextCursor": {
+ "oneOf": [
+ {
+ "type": "string"
},
- "usersAccesses": {
- "type": ["object"],
- "description": "A map of user identifiers to permissions list."
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Room"
+ }
+ }
+ },
+ "example": {
+ "nextCursor": "eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+ "data": [
+ {
+ "type": "room",
+ "id": "my-room-id",
+ "lastConnectionAt": "2022-08-08T23:23:15.281Z",
+ "createdAt": "2022-08-08T23:23:15.281Z",
+ "organizationId": "org_123456789",
+ "metadata": {
+ "color": "blue"
},
+ "defaultAccesses": ["room:write"],
"groupsAccesses": {
- "type": ["object"],
- "description": "A map of group identifiers to permissions list."
+ "product": ["room:write"]
},
- "metadata": {
- "type": ["object"],
- "description": "A map of metadata keys to their values (`string` or `string[]`)."
+ "usersAccesses": {
+ "alice": ["room:write"]
}
- },
- "required": ["defaultAccesses"]
- }
- },
- "required": ["update"]
+ }
+ ]
+ }
},
- "CreateRoom": {
- "title": "CreateRoom",
+ "CreateRoomRequestBody": {
+ "title": "CreateRoomRequestBody",
"type": "object",
"properties": {
"id": {
"type": "string"
},
+ "organizationId": {
+ "type": "string",
+ "description": "The organization ID to associate with the room. Defaults to \"default\" if not provided."
+ },
"defaultAccesses": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "$ref": "#/components/schemas/RoomPermission"
},
"usersAccesses": {
- "type": "object"
+ "$ref": "#/components/schemas/RoomAccesses"
},
"groupsAccesses": {
- "type": "object"
+ "$ref": "#/components/schemas/RoomAccesses"
},
"metadata": {
- "type": "object"
+ "$ref": "#/components/schemas/RoomMetadata"
},
"engine": {
"type": "integer",
"enum": [1, 2],
"description": "Preferred storage engine version to use when creating new rooms. The v2 Storage engine supports larger documents, is more performant, has native streaming support, and will become the default in the future."
+ },
+ "organizationId": {
+ "type": "string"
}
},
- "required": ["id", "defaultAccesses"]
- },
- "Error": {
- "title": "Error",
- "type": "object",
- "properties": {
- "error": {
- "type": "string",
- "description": "Error code"
- },
- "message": {
- "type": "string",
- "description": "Message explaining the error"
+ "required": ["id", "defaultAccesses"],
+ "additionalProperties": false,
+ "example": {
+ "id": "my-room-id",
+ "defaultAccesses": ["room:write"],
+ "metadata": {
+ "color": "blue"
},
- "suggestion": {
- "type": "string",
- "description": "A suggestion on how to fix the error"
+ "usersAccesses": {
+ "alice": ["room:write"]
},
- "docs": {
- "type": "string",
- "description": "A link to the documentation"
- }
- }
- },
- "Authorization": {
- "title": "Authorization",
- "type": "object",
- "properties": {
- "token": {
- "type": "string"
- }
- }
- },
- "TokenResponse": {
- "title": "An HTTP response body containing a token.",
- "type": "object",
- "properties": {
- "token": {
- "type": "string"
+ "groupsAccesses": {
+ "product": ["room:write"]
}
}
},
- "AuthorizeUserRequest": {
- "title": "AuthorizeUserRequest",
+ "UpdateRoomRequestBody": {
"type": "object",
+ "title": "UpdateRoomRequestBody",
+ "additionalProperties": false,
"properties": {
- "userId": {
- "type": "string"
+ "defaultAccesses": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/RoomPermission"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
- "userInfo": {
- "type": "object"
+ "usersAccesses": {
+ "type": "object",
+ "description": "A map of user identifiers to permissions list. Setting the value as `null` will clear all users’ accesses. Setting one user identifier as `null` will clear this user’s accesses.",
+ "additionalProperties": {
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "room:write",
+ "room:read",
+ "room:presence:write",
+ "comments:write"
+ ]
+ }
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
},
- "organizationId": {
- "type": "string"
+ "groupsAccesses": {
+ "type": "object",
+ "description": "A map of group identifiers to permissions list. Setting the value as `null` will clear all groups’ accesses. Setting one group identifier as `null` will clear this group’s accesses.",
+ "additionalProperties": {
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "room:write",
+ "room:read",
+ "room:presence:write",
+ "comments:write"
+ ]
+ }
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
},
- "permissions": {
+ "metadata": {
"type": "object",
"additionalProperties": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ]
}
}
- }
- },
- "IdentifyUserRequest": {
- "title": "IdentifyUserRequest",
- "type": "object",
- "properties": {
- "userId": {
- "type": "string"
- },
- "organizationId": {
- "type": "string"
+ },
+ "example": {
+ "defaultAccesses": ["room:write"],
+ "usersAccesses": {
+ "alice": ["room:write"]
},
- "groupIds": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "groupsAccesses": {
+ "marketing": ["room:write"]
},
- "userInfo": {
- "type": "object"
+ "metadata": {
+ "color": "blue"
}
}
},
- "CreateAuthorization": {
- "title": "CreateAuthorization",
+ "UpsertRoomRequestBody": {
"type": "object",
+ "title": "UpsertRoomRequestBody",
"properties": {
- "userId": {
- "type": "string"
- },
- "userInfo": {
- "type": "object"
+ "update": {
+ "$ref": "#/components/schemas/UpdateRoomRequestBody"
},
- "groupIds": {
- "type": "array",
- "items": {
- "type": "string"
+ "create": {
+ "$ref": "#/components/schemas/CreateRoomRequestBody"
+ }
+ },
+ "required": ["update"],
+ "additionalProperties": false,
+ "example": {
+ "update": {
+ "usersAccesses": {
+ "alice": ["room:write"]
+ },
+ "groupsAccesses": {
+ "marketing": ["room:write"]
+ },
+ "metadata": {
+ "color": "blue"
}
+ },
+ "create": {
+ "defaultAccesses": ["room:write"]
}
}
},
- "GetRooms": {
- "title": "GetRooms",
+ "UpdateRoomIdRequestBody": {
"type": "object",
+ "title": "UpdateRoomIdRequestBody",
+ "additionalProperties": false,
"properties": {
- "nextCursor": {
- "type": "string"
- },
- "data": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string"
- },
- "lastConnectionAt": {
- "type": "string"
- },
- "createdAt": {
- "type": "string"
- },
- "metadata": {
- "type": "object"
- },
- "defaultAccesses": {
- "type": ["string", "array"],
- "items": {}
- },
- "usersAccesses": {
- "type": "object"
- },
- "groupsAccesses": {
- "type": "object"
- }
- }
- }
+ "newRoomId": {
+ "type": "string",
+ "description": "The new room ID"
}
+ },
+ "required": ["newRoomId"],
+ "example": {
+ "newRoomId": "new-room-id"
}
},
- "GetYjsVersions": {
- "title": "GetYjsVersions",
+ "ActiveUsersResponse": {
+ "title": "ActiveUsersResponse",
"type": "object",
"properties": {
- "nextCursor": {
- "type": "string",
- "description": "Cursor for pagination to get the next page of results"
- },
"data": {
"type": "array",
"items": {
@@ -6451,208 +6474,663 @@
"properties": {
"type": {
"type": "string",
- "enum": ["historyVersion"],
- "description": "Type identifier for the version history object"
+ "const": "user"
},
"id": {
- "type": "string",
- "description": "Unique identifier for the version"
- },
- "createdAt": {
- "type": "string",
- "format": "date-time",
- "description": "ISO 8601 timestamp of when the version was created"
- },
- "authors": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "User ID of the author"
- }
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
}
- },
- "description": "List of users who contributed to this version"
+ ]
},
- "kind": {
- "type": "string",
- "enum": ["yjs"],
- "description": "Type of document (yjs for Yjs documents)"
+ "info": {
+ "type": "object",
+ "additionalProperties": true
+ },
+ "connectionId": {
+ "type": "integer"
}
},
- "required": ["type", "id", "createdAt", "authors", "kind"]
+ "required": ["type", "id", "info", "connectionId"]
}
}
},
- "required": ["data"]
+ "required": ["data"],
+ "example": {
+ "data": [
+ {
+ "type": "user",
+ "connectionId": 16,
+ "id": "alice",
+ "info": {}
+ },
+ {
+ "type": "user",
+ "connectionId": 20,
+ "id": "bob",
+ "info": {}
+ }
+ ]
+ }
},
- "CreateYjsVersion": {
- "title": "CreateYjsVersion",
+ "SetPresenceRequestBody": {
+ "title": "SetPresenceRequestBody",
"type": "object",
"properties": {
- "data": {
+ "userId": {
+ "type": "string",
+ "description": "ID of the user to set presence for"
+ },
+ "data": {
+ "type": "object",
+ "description": "Presence data as a JSON object",
+ "additionalProperties": true
+ },
+ "userInfo": {
"type": "object",
"properties": {
- "id": {
+ "name": {
"type": "string",
- "description": "Unique identifier for the created version"
+ "minLength": 1,
+ "description": "Optional name for the user or agent"
+ },
+ "avatar": {
+ "type": "string",
+ "format": "uri",
+ "description": "Optional avatar URL for the user"
+ },
+ "color": {
+ "type": "string",
+ "description": "Optional color for the user"
}
},
- "required": ["id"]
+ "description": "Metadata about the user or agent",
+ "additionalProperties": true
+ },
+ "ttl": {
+ "type": "integer",
+ "minimum": 2,
+ "maximum": 3599,
+ "description": "Time-to-live in seconds (minimum: 2, maximum: 3599). After this duration, the presence will automatically expire."
}
},
- "required": ["data"]
+ "required": ["userId", "data"],
+ "additionalProperties": false,
+ "example": {
+ "userId": "agent-123",
+ "data": {
+ "status": "active",
+ "cursor": {
+ "x": 100,
+ "y": 200
+ }
+ },
+ "userInfo": {
+ "name": "AI Assistant",
+ "avatar": "https://example.org/images/agent123.jpg"
+ },
+ "ttl": 60
+ }
},
- "ActiveUsersResponse": {
- "title": "ActiveUsersResponse",
+ "GetStorageDocumentResponse": {
+ "title": "GetStorageDocumentResponse",
+ "type": "object",
+ "additionalProperties": true,
+ "example": {
+ "liveblocksType": "LiveObject",
+ "data": {
+ "aLiveObject": {
+ "liveblocksType": "LiveObject",
+ "data": {
+ "a": 1
+ }
+ },
+ "aLiveList": {
+ "liveblocksType": "LiveList",
+ "data": ["a", "b"]
+ },
+ "aLiveMap": {
+ "liveblocksType": "LiveMap",
+ "data": {
+ "a": 1,
+ "b": 2
+ }
+ }
+ }
+ }
+ },
+ "InitializeStorageDocumentResponse": {
+ "title": "InitializeStorageDocumentResponse",
"type": "object",
"properties": {
+ "liveblocksType": {
+ "type": "string",
+ "const": "LiveObject"
+ },
"data": {
- "type": "array",
- "items": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ },
+ "example": {
+ "liveblocksType": "LiveObject",
+ "data": {
+ "aLiveObject": {
+ "liveblocksType": "LiveObject",
+ "data": {
+ "a": 1
+ }
+ },
+ "aLiveList": {
+ "liveblocksType": "LiveList",
+ "data": ["a", "b"]
+ }
+ }
+ }
+ },
+ "PatchStorageDocumentRequestBody": {
+ "title": "PatchStorageDocumentRequestBody",
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "title": "AddJsonPatchOperation",
"type": "object",
"properties": {
- "type": {
- "type": "string"
+ "op": {
+ "type": "string",
+ "const": "add"
},
- "connectionId": {
- "type": "number"
+ "path": {
+ "type": "string",
+ "description": "A JSON Pointer to the target location (RFC 6901). Must start with \"/\"."
+ },
+ "value": {}
+ },
+ "required": ["op", "path", "value"],
+ "additionalProperties": false
+ },
+ {
+ "title": "RemoveJsonPatchOperation",
+ "type": "object",
+ "properties": {
+ "op": {
+ "type": "string",
+ "const": "remove"
+ },
+ "path": {
+ "type": "string",
+ "description": "A JSON Pointer to the target location (RFC 6901). Must start with \"/\"."
}
+ },
+ "required": ["op", "path"],
+ "additionalProperties": false
+ },
+ {
+ "title": "ReplaceJsonPatchOperation",
+ "type": "object",
+ "properties": {
+ "op": {
+ "type": "string",
+ "const": "replace"
+ },
+ "path": {
+ "type": "string",
+ "description": "A JSON Pointer to the target location (RFC 6901). Must start with \"/\"."
+ },
+ "value": {}
}
+ },
+ {
+ "title": "CopyJsonPatchOperation",
+ "type": "object",
+ "properties": {
+ "op": {
+ "type": "string",
+ "const": "copy"
+ },
+ "from": {
+ "type": "string",
+ "description": "A JSON Pointer to the source location (RFC 6901). Must start with \"/\"."
+ },
+ "path": {
+ "type": "string",
+ "description": "A JSON Pointer to the target location (RFC 6901). Must start with \"/\"."
+ }
+ },
+ "required": ["op", "from", "path"],
+ "additionalProperties": false
+ },
+ {
+ "title": "MoveJsonPatchOperation",
+ "type": "object",
+ "properties": {
+ "op": {
+ "type": "string",
+ "const": "move"
+ },
+ "from": {
+ "type": "string",
+ "description": "A JSON Pointer to the source location (RFC 6901). Must start with \"/\"."
+ },
+ "path": {
+ "type": "string",
+ "description": "A JSON Pointer to the target location (RFC 6901). Must start with \"/\"."
+ }
+ },
+ "required": ["op", "from", "path"],
+ "additionalProperties": false
+ },
+ {
+ "title": "TestJsonPatchOperation",
+ "type": "object",
+ "properties": {
+ "op": {
+ "type": "string",
+ "const": "test"
+ },
+ "path": {
+ "type": "string",
+ "description": "A JSON Pointer to the target location (RFC 6901). Must start with \"/\"."
+ },
+ "value": {}
+ },
+ "required": ["op", "path", "value"],
+ "additionalProperties": false
}
+ ]
+ },
+ "example": [
+ {
+ "op": "add",
+ "path": "/score",
+ "value": 42
+ },
+ {
+ "op": "remove",
+ "path": "/oldKey"
}
- }
+ ]
},
- "PublicAuthorizeBodyRequest": {
- "title": "PublicAuthorizeBodyRequest",
+ "GetYjsDocumentResponse": {
+ "title": "GetYjsDocumentResponse",
"type": "object",
- "properties": {
- "publicApiKey": {
- "type": "string"
- }
+ "additionalProperties": true,
+ "example": {
+ "someYText": "Contents of YText"
}
},
- "SchemaResponse": {
- "title": "Schema",
+ "Error": {
+ "title": "Error",
"type": "object",
"properties": {
- "id": {
- "type": "string"
- },
- "name": {
- "type": "string"
- },
- "version": {
- "type": "number"
+ "error": {
+ "type": "string",
+ "description": "Error code"
},
- "createdAt": {
+ "message": {
"type": "string",
- "format": "date-time"
+ "description": "Message explaining the error"
},
- "updatedAt": {
+ "suggestion": {
"type": "string",
- "format": "date-time"
+ "description": "A suggestion on how to fix the error"
},
- "body": {
- "type": "string"
+ "docs": {
+ "type": "string",
+ "description": "A link to the documentation"
}
}
},
- "SchemaRequest": {
- "title": "SchemaRequest",
+ "Authorization": {
+ "title": "Authorization",
"type": "object",
"properties": {
- "name": {
+ "token": {
"type": "string"
- },
- "body": {
+ }
+ },
+ "example": {
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi..."
+ }
+ },
+ "AuthorizeUserResponse": {
+ "title": "AuthorizeUserResponse",
+ "type": "object",
+ "properties": {
+ "token": {
"type": "string"
}
+ },
+ "required": ["token"],
+ "additionalProperties": false,
+ "example": {
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi..."
}
},
- "UpdateSchema": {
- "title": "UpdateSchema",
+ "IdentifyUserResponse": {
+ "title": "IdentifyUserResponse",
"type": "object",
"properties": {
- "body": {
+ "token": {
"type": "string"
}
+ },
+ "required": ["token"],
+ "additionalProperties": false,
+ "example": {
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi..."
}
},
- "Thread": {
+ "AuthorizeUserRequestBody": {
+ "title": "AuthorizeUserRequestBody",
"type": "object",
- "title": "Thread",
"properties": {
- "type": {
- "const": "thread"
+ "userId": {
+ "type": "string"
},
- "id": {
+ "userInfo": {
+ "type": "object"
+ },
+ "organizationId": {
"type": "string"
},
- "roomId": {
+ "permissions": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "required": ["userId", "permissions"],
+ "additionalProperties": false,
+ "example": {
+ "userId": "user-123",
+ "userInfo": {
+ "name": "bob",
+ "avatar": "https://example.org/images/user123.jpg"
+ },
+ "organizationId": "acme-corp",
+ "permissions": {
+ "my-room-1": ["room:write"],
+ "my-room-2": ["room:write"],
+ "my-room-*": ["room:read"]
+ }
+ }
+ },
+ "IdentifyUserRequestBody": {
+ "title": "IdentifyUserRequestBody",
+ "type": "object",
+ "properties": {
+ "userId": {
"type": "string"
},
- "comments": {
+ "organizationId": {
+ "type": "string"
+ },
+ "groupIds": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/Comment"
+ "type": "string"
}
},
- "createdAt": {
+ "userInfo": {
+ "type": "object"
+ }
+ },
+ "required": ["userId"],
+ "additionalProperties": false,
+ "example": {
+ "userId": "user-123",
+ "organizationId": "acme-corp",
+ "groupIds": ["marketing", "engineering"],
+ "userInfo": {
+ "name": "bob",
+ "avatar": "https://example.org/images/user123.jpg"
+ }
+ }
+ },
+ "YjsVersion": {
+ "title": "YjsVersion",
+ "type": "object",
+ "properties": {
+ "id": {
"type": "string",
- "format": "date-time"
+ "description": "Unique identifier for the version"
},
- "metadata": {
- "type": "object"
+ "type": {
+ "type": "string",
+ "const": "historyVersion"
},
- "resolved": {
- "type": "boolean"
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "description": "ISO 8601 timestamp of when the version was created"
},
- "updatedAt": {
+ "authors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "User ID of the author"
+ }
+ }
+ },
+ "description": "List of users who contributed to this version"
+ },
+ "kind": {
"type": "string",
- "format": "date-time"
+ "const": "yjs"
}
},
- "required": [
- "type",
- "id",
- "roomId",
- "comments",
- "createdAt",
- "metadata"
- ],
- "examples": [
- {
- "type": "thread",
- "id": "thread-id",
- "roomId": "room-id",
- "comments": [
+ "required": ["id", "type", "createdAt", "kind"],
+ "example": {
+ "id": "vh_abc123",
+ "type": "historyVersion",
+ "createdAt": "2024-10-15T10:30:00.000Z",
+ "authors": [
+ {
+ "id": "user-123"
+ },
+ {
+ "id": "user-456"
+ }
+ ],
+ "kind": "yjs"
+ }
+ },
+ "GetYjsVersionsResponse": {
+ "title": "GetYjsVersionsResponse",
+ "type": "object",
+ "properties": {
+ "nextCursor": {
+ "description": "Cursor for pagination to get the next page of results",
+ "oneOf": [
{
- "type": "comment",
- "threadId": "thread-id",
- "roomId": "room-id",
- "id": "comment-id",
- "userId": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "editedAt": "2019-08-24T14:15:22Z",
- "deletedAt": "2019-08-24T14:15:22Z",
- "body": {},
- "metadata": {},
- "reactions": [],
- "attachments": []
+ "type": "string"
+ },
+ {
+ "type": "null"
}
- ],
- "createdAt": "2019-08-24T14:15:22Z",
- "metadata": {},
- "updatedAt": "2019-08-24T14:15:22Z"
+ ]
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/YjsVersion"
+ }
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "example": {
+ "data": [
+ {
+ "type": "historyVersion",
+ "id": "vh_abc123",
+ "createdAt": "2024-10-15T10:30:00.000Z",
+ "authors": [
+ {
+ "id": "user-123"
+ }
+ ],
+ "kind": "yjs"
+ }
+ ],
+ "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI0LTEwLTE1VDEwOjMwOjAwLjAwMFoifQ=="
+ }
+ },
+ "CreateYjsVersionResponse": {
+ "title": "CreateYjsVersionResponse",
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique identifier for the created version"
+ }
+ },
+ "required": ["id"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "example": {
+ "data": {
+ "id": "vh_abc123"
+ }
+ }
+ },
+ "GetThreadsResponse": {
+ "title": "GetThreadsResponse",
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Thread"
+ }
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "example": {
+ "data": [
+ {
+ "type": "thread",
+ "id": "th_abc123",
+ "roomId": "my-room-id",
+ "comments": [
+ {
+ "type": "comment",
+ "threadId": "th_abc123",
+ "roomId": "my-room-id",
+ "id": "cm_abc123",
+ "userId": "alice",
+ "createdAt": "2022-07-13T14:32:50.697Z",
+ "body": {
+ "version": 1,
+ "content": []
+ },
+ "metadata": {},
+ "reactions": [],
+ "attachments": []
+ }
+ ],
+ "createdAt": "2022-07-13T14:32:50.697Z",
+ "updatedAt": "2022-07-13T14:32:50.697Z",
+ "metadata": {},
+ "resolved": false
+ }
+ ]
+ }
+ },
+ "Thread": {
+ "type": "object",
+ "title": "Thread",
+ "properties": {
+ "type": {
+ "const": "thread"
+ },
+ "id": {
+ "type": "string"
+ },
+ "roomId": {
+ "type": "string"
+ },
+ "comments": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Comment"
+ }
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "metadata": {
+ "$ref": "#/components/schemas/ThreadMetadata"
+ },
+ "resolved": {
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time"
}
+ },
+ "required": [
+ "type",
+ "id",
+ "roomId",
+ "comments",
+ "createdAt",
+ "updatedAt",
+ "metadata",
+ "resolved"
],
- "description": ""
+ "example": {
+ "type": "thread",
+ "id": "th_abc123",
+ "roomId": "my-room-id",
+ "comments": [
+ {
+ "type": "comment",
+ "threadId": "th_abc123",
+ "roomId": "my-room-id",
+ "id": "cm_abc123",
+ "userId": "alice",
+ "createdAt": "2022-07-13T14:32:50.697Z",
+ "body": {
+ "version": 1,
+ "content": []
+ },
+ "metadata": {},
+ "reactions": [],
+ "attachments": []
+ }
+ ],
+ "createdAt": "2022-07-13T14:32:50.697Z",
+ "updatedAt": "2022-07-13T14:32:50.697Z",
+ "metadata": {
+ "color": "blue"
+ },
+ "resolved": false
+ }
},
- "CreateThread": {
- "title": "CreateThread",
+ "CreateThreadRequestBody": {
+ "title": "CreateThreadRequestBody",
"type": "object",
"properties": {
"comment": {
@@ -6666,27 +7144,17 @@
"format": "date-time"
},
"body": {
- "type": "object",
- "properties": {
- "version": {
- "type": "number"
- },
- "content": {
- "type": "array",
- "items": {
- "type": "object"
- }
- }
- }
+ "$ref": "#/components/schemas/CommentBody"
},
"metadata": {
"$ref": "#/components/schemas/CommentMetadata"
}
},
- "required": ["userId", "body"]
+ "required": ["userId", "body"],
+ "additionalProperties": false
},
"metadata": {
- "type": "object"
+ "$ref": "#/components/schemas/ThreadMetadata"
}
},
"required": ["comment"],
@@ -6708,15 +7176,47 @@
"color": "blue"
}
}
- ],
- "description": ""
+ ]
+ },
+ "GetThreadParticipantsResponse": {
+ "title": "GetThreadParticipantsResponse",
+ "type": "object",
+ "properties": {
+ "participantIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["participantIds"],
+ "additionalProperties": false,
+ "example": {
+ "participantIds": ["user-1", "user-2"]
+ }
},
- "UpdateThreadMetadata": {
- "title": "UpdateThreadMetadata",
+ "EditThreadMetadataRequestBody": {
+ "title": "EditThreadMetadataRequestBody",
"type": "object",
"properties": {
"metadata": {
- "type": "object"
+ "type": "object",
+ "additionalProperties": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
},
"userId": {
"type": "string"
@@ -6735,13 +7235,25 @@
"userId": "alice",
"createdAt": "2023-07-13T14:32:50.697Z"
}
- ],
- "description": ""
+ ]
},
"ThreadMetadata": {
"type": "object",
"title": "ThreadMetadata",
- "additionalProperties": true,
+ "additionalProperties": {
+ "oneOf": [
+ {
+ "type": "string",
+ "maxLength": 4000
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ }
+ ]
+ },
"required": [],
"examples": [
{
@@ -6749,27 +7261,64 @@
"age": 25
}
],
- "description": ""
+ "description": "Custom metadata attached to a thread. Supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.",
+ "maxProperties": 50,
+ "propertyNames": {
+ "maxLength": 40
+ }
},
"CommentBody": {
"type": "object",
"title": "CommentBody",
"properties": {
"version": {
- "type": "number"
+ "type": "integer"
},
"content": {
"type": "array",
"items": {
- "type": "object"
+ "type": "object",
+ "additionalProperties": true
}
}
+ },
+ "required": ["version", "content"],
+ "additionalProperties": false,
+ "example": {
+ "version": 1,
+ "content": [
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "text": "Hello "
+ },
+ {
+ "text": "world",
+ "bold": true
+ }
+ ]
+ }
+ ]
}
},
"CommentMetadata": {
"type": "object",
"title": "CommentMetadata",
- "additionalProperties": true,
+ "additionalProperties": {
+ "oneOf": [
+ {
+ "type": "string",
+ "maxLength": 4000
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ }
+ ]
+ },
"required": [],
"examples": [
{
@@ -6777,14 +7326,34 @@
"spam": false
}
],
- "description": ""
+ "description": "Custom metadata attached to a comment. Supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.",
+ "maxProperties": 50,
+ "propertyNames": {
+ "maxLength": 40
+ }
},
- "UpdateCommentMetadata": {
- "title": "UpdateCommentMetadata",
+ "EditCommentMetadataRequestBody": {
+ "title": "EditCommentMetadataRequestBody",
"type": "object",
"properties": {
"metadata": {
- "type": "object"
+ "type": "object",
+ "additionalProperties": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
},
"userId": {
"type": "string"
@@ -6795,6 +7364,7 @@
}
},
"required": ["metadata", "userId"],
+ "additionalProperties": false,
"examples": [
{
"metadata": {
@@ -6803,18 +7373,14 @@
"userId": "alice",
"updatedAt": "2023-07-13T14:32:50.697Z"
}
- ],
- "description": ""
+ ]
},
"Comment": {
"type": "object",
"title": "Comment",
"properties": {
"type": {
- "const": "comment",
- "readOnly": true,
- "default": "comment",
- "examples": ["comment"]
+ "const": "comment"
},
"threadId": {
"type": "string"
@@ -6845,9 +7411,31 @@
},
"metadata": {
"$ref": "#/components/schemas/CommentMetadata"
+ },
+ "reactions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/CommentReaction"
+ }
+ },
+ "attachments": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/CommentAttachment"
+ }
}
},
- "required": ["type", "threadId", "roomId", "id", "userId", "createdAt"],
+ "required": [
+ "type",
+ "threadId",
+ "roomId",
+ "id",
+ "userId",
+ "createdAt",
+ "metadata",
+ "reactions",
+ "attachments"
+ ],
"examples": [
{
"type": "comment",
@@ -6865,8 +7453,8 @@
}
]
},
- "CreateComment": {
- "title": "CreateComment",
+ "CreateCommentRequestBody": {
+ "title": "CreateCommentRequestBody",
"type": "object",
"properties": {
"userId": {
@@ -6881,8 +7469,16 @@
},
"metadata": {
"$ref": "#/components/schemas/CommentMetadata"
+ },
+ "attachmentIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 10
}
},
+ "additionalProperties": false,
"required": ["userId", "body"],
"examples": [
{
@@ -6897,11 +7493,10 @@
"spam": false
}
}
- ],
- "description": ""
+ ]
},
- "UpdateComment": {
- "title": "UpdateComment",
+ "EditCommentRequestBody": {
+ "title": "EditCommentRequestBody",
"type": "object",
"properties": {
"editedAt": {
@@ -6913,9 +7508,17 @@
},
"metadata": {
"$ref": "#/components/schemas/CommentMetadata"
+ },
+ "attachmentIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 10
}
},
"required": ["body"],
+ "additionalProperties": false,
"examples": [
{
"editedAt": "2022-07-13T14:32:50.697Z",
@@ -6928,8 +7531,89 @@
"spam": false
}
}
- ],
- "description": ""
+ ]
+ },
+ "MarkThreadAsResolvedRequestBody": {
+ "type": "object",
+ "title": "MarkThreadAsResolvedRequestBody",
+ "required": ["userId"],
+ "properties": {
+ "userId": {
+ "type": "string",
+ "description": "The user ID of the user who marked the thread as resolved."
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "userId": "alice"
+ }
+ },
+ "MarkThreadAsUnresolvedRequestBody": {
+ "type": "object",
+ "title": "MarkThreadAsUnresolvedRequestBody",
+ "required": ["userId"],
+ "properties": {
+ "userId": {
+ "type": "string",
+ "description": "The user ID of the user who marked the thread as unresolved."
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "userId": "alice"
+ }
+ },
+ "SubscribeToThreadRequestBody": {
+ "type": "object",
+ "title": "SubscribeToThreadRequestBody",
+ "required": ["userId"],
+ "properties": {
+ "userId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "userId": "alice"
+ }
+ },
+ "UnsubscribeFromThreadRequestBody": {
+ "type": "object",
+ "title": "UnsubscribeFromThreadRequestBody",
+ "required": ["userId"],
+ "properties": {
+ "userId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "userId": "alice"
+ }
+ },
+ "GetThreadSubscriptionsResponse": {
+ "type": "object",
+ "title": "GetThreadSubscriptionsResponse",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/UserSubscription"
+ }
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "example": {
+ "data": [
+ {
+ "kind": "thread",
+ "subjectId": "th_abc123",
+ "createdAt": "2022-07-13T14:32:50.697Z",
+ "userId": "alice"
+ }
+ ]
+ }
},
"CommentReaction": {
"type": "object",
@@ -6946,7 +7630,8 @@
"type": "string"
}
},
- "required": ["userId", "emoji"],
+ "required": ["userId", "emoji", "createdAt"],
+ "additionalProperties": false,
"examples": [
{
"emoji": "👨👩👧",
@@ -6955,11 +7640,42 @@
}
]
},
- "AddCommentReaction": {
+ "CommentAttachment": {
"type": "object",
- "title": "AddCommentReaction",
+ "title": "CommentAttachment",
"properties": {
- "userId": {
+ "type": {
+ "type": "string",
+ "const": "attachment"
+ },
+ "id": {
+ "type": "string"
+ },
+ "mimeType": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "size": {
+ "type": "integer"
+ }
+ },
+ "required": ["type", "id", "mimeType", "name", "size"],
+ "additionalProperties": false,
+ "example": {
+ "type": "attachment",
+ "id": "at_abc123",
+ "mimeType": "image/png",
+ "name": "screenshot.png",
+ "size": 12345
+ }
+ },
+ "AddCommentReactionRequestBody": {
+ "type": "object",
+ "title": "AddCommentReactionRequestBody",
+ "properties": {
+ "userId": {
"type": "string"
},
"createdAt": {
@@ -6971,6 +7687,7 @@
}
},
"required": ["userId", "emoji"],
+ "additionalProperties": false,
"examples": [
{
"emoji": "👨👩👧",
@@ -6979,9 +7696,9 @@
}
]
},
- "RemoveCommentReaction": {
+ "RemoveCommentReactionRequestBody": {
"type": "object",
- "title": "RemoveCommentReaction",
+ "title": "RemoveCommentReactionRequestBody",
"properties": {
"userId": {
"type": "string"
@@ -6995,6 +7712,7 @@
}
},
"required": ["userId", "emoji"],
+ "additionalProperties": false,
"examples": [
{
"emoji": "👨👩👧",
@@ -7003,6 +7721,50 @@
}
]
},
+ "GetInboxNotificationsResponse": {
+ "type": "object",
+ "properties": {
+ "nextCursor": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "A cursor to use for pagination. Pass this value as `startingAfter` to get the next page of results. `null` if there are no more results."
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/InboxNotificationThreadData"
+ },
+ {
+ "$ref": "#/components/schemas/InboxNotificationCustomData"
+ }
+ ]
+ }
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "example": {
+ "nextCursor": null,
+ "data": [
+ {
+ "kind": "thread",
+ "id": "in_abc123",
+ "roomId": "my-room-id",
+ "threadId": "th_abc123",
+ "notifiedAt": "2024-01-15T10:30:00.000Z",
+ "readAt": null
+ }
+ ]
+ }
+ },
"InboxNotificationThreadData": {
"title": "InboxNotificationThreadData",
"type": "object",
@@ -7020,12 +7782,74 @@
"type": "string"
},
"readAt": {
+ "oneOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "notifiedAt": {
"type": "string",
"format": "date-time"
+ }
+ },
+ "required": [
+ "kind",
+ "id",
+ "roomId",
+ "threadId",
+ "notifiedAt",
+ "readAt"
+ ],
+ "additionalProperties": false,
+ "example": {
+ "kind": "thread",
+ "id": "in_abc123",
+ "roomId": "my-room-id",
+ "threadId": "th_abc123",
+ "notifiedAt": "2024-01-15T10:30:00.000Z",
+ "readAt": null
+ }
+ },
+ "InboxNotificationActivity": {
+ "title": "InboxNotificationActivity",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
},
- "notifiedAt": {
+ "createdAt": {
"type": "string",
"format": "date-time"
+ },
+ "data": {
+ "type": "object",
+ "additionalProperties": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ }
+ }
+ },
+ "required": ["id", "createdAt", "data"],
+ "additionalProperties": false,
+ "example": {
+ "id": "act_abc123",
+ "createdAt": "2024-01-15T10:30:00.000Z",
+ "data": {
+ "url": "url-to-file"
}
}
},
@@ -7037,25 +7861,69 @@
"type": "string"
},
"kind": {
- "type": "string"
+ "type": "string",
+ "const": "custom"
},
"subjectId": {
"type": "string"
},
"roomId": {
- "type": "string"
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
"readAt": {
- "type": "string",
- "format": "date-time"
+ "oneOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
"notifiedAt": {
"type": "string",
"format": "date-time"
},
- "activityData": {
- "type": "object"
+ "activities": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/InboxNotificationActivity"
+ }
}
+ },
+ "required": [
+ "kind",
+ "id",
+ "subjectId",
+ "notifiedAt",
+ "readAt",
+ "activities"
+ ],
+ "additionalProperties": false,
+ "example": {
+ "kind": "custom",
+ "id": "in_xyz789",
+ "subjectId": "file123",
+ "roomId": null,
+ "notifiedAt": "2024-01-15T10:30:00.000Z",
+ "readAt": null,
+ "activities": [
+ {
+ "id": "act_abc123",
+ "createdAt": "2024-01-15T10:30:00.000Z",
+ "data": {
+ "url": "url-to-file"
+ }
+ }
+ ]
}
},
"NotificationChannelSettings": {
@@ -7071,6 +7939,11 @@
"additionalProperties": {
"type": "boolean",
"description": "Custom notification kinds prefixed by a '$'"
+ },
+ "example": {
+ "thread": true,
+ "textMention": false,
+ "$customNotification": true
}
},
"NotificationSettings": {
@@ -7089,9 +7962,33 @@
"$ref": "#/components/schemas/NotificationChannelSettings"
}
},
- "description": "Notification settings for each supported channel"
+ "additionalProperties": false,
+ "description": "Notification settings for each supported channel",
+ "example": {
+ "email": {
+ "thread": true,
+ "textMention": false,
+ "$customNotification": true
+ },
+ "slack": {
+ "thread": true,
+ "textMention": true,
+ "$customNotification": false
+ },
+ "teams": {
+ "thread": false,
+ "textMention": true,
+ "$customNotification": true
+ },
+ "webPush": {
+ "thread": true,
+ "textMention": true,
+ "$customNotification": false
+ }
+ }
},
- "PartialNotificationSettings": {
+ "UpdateNotificationSettingsRequestBody": {
+ "title": "UpdateNotificationSettingsRequestBody",
"type": "object",
"properties": {
"email": {
@@ -7107,7 +8004,54 @@
"$ref": "#/components/schemas/NotificationChannelSettings"
}
},
- "description": "Partial notification settings - all properties are optional"
+ "additionalProperties": false,
+ "description": "Partial notification settings - all properties are optional",
+ "example": {
+ "email": {
+ "thread": true,
+ "textMention": false
+ },
+ "slack": {
+ "textMention": false
+ },
+ "webPush": {
+ "thread": true
+ }
+ }
+ },
+ "GetRoomSubscriptionSettingsResponse": {
+ "type": "object",
+ "properties": {
+ "nextCursor": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "A cursor to use for pagination. Pass this value as `startingAfter` to get the next page of results. `null` if there are no more results."
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/UserRoomSubscriptionSettings"
+ }
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "example": {
+ "nextCursor": null,
+ "data": [
+ {
+ "threads": "all",
+ "textMentions": "mine",
+ "roomId": "my-room-id"
+ }
+ ]
+ }
},
"RoomSubscriptionSettings": {
"title": "RoomSubscriptionSettings",
@@ -7119,6 +8063,30 @@
"textMentions": {
"enum": ["mine", "none"]
}
+ },
+ "required": ["threads", "textMentions"],
+ "additionalProperties": false,
+ "example": {
+ "threads": "all",
+ "textMentions": "mine"
+ }
+ },
+ "UpdateRoomSubscriptionSettingsRequestBody": {
+ "title": "UpdateRoomSubscriptionSettingsRequestBody",
+ "type": "object",
+ "properties": {
+ "threads": {
+ "enum": ["all", "replies_and_mentions", "none"]
+ },
+ "textMentions": {
+ "enum": ["mine", "none"]
+ }
+ },
+ "additionalProperties": false,
+ "description": "Partial room subscription settings - all properties are optional",
+ "example": {
+ "threads": "replies_and_mentions",
+ "textMentions": "none"
}
},
"UserRoomSubscriptionSettings": {
@@ -7126,6 +8094,7 @@
"type": "object",
"properties": {
"threads": {
+ "type": "string",
"enum": ["all", "replies_and_mentions", "none"]
},
"textMentions": {
@@ -7134,10 +8103,17 @@
"roomId": {
"type": "string"
}
+ },
+ "required": ["threads", "textMentions", "roomId"],
+ "additionalProperties": false,
+ "example": {
+ "threads": "all",
+ "textMentions": "mine",
+ "roomId": "my-room-id"
}
},
- "TriggerInboxNotification": {
- "title": "TriggerInboxNotification",
+ "TriggerInboxNotificationRequestBody": {
+ "title": "TriggerInboxNotificationRequestBody",
"type": "object",
"properties": {
"userId": {
@@ -7153,20 +8129,34 @@
"type": "string"
},
"activityData": {
- "type": "object"
+ "type": "object",
+ "additionalProperties": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ }
+ ]
+ }
+ },
+ "organizationId": {
+ "type": "string"
}
},
"required": ["userId", "kind", "subjectId", "activityData"],
- "examples": [
- {
- "userId": "alice",
- "kind": "file-uploaded",
- "subjectId": "file123",
- "activityData": {
- "url": "url-to-file"
- }
+ "example": {
+ "userId": "alice",
+ "kind": "file-uploaded",
+ "subjectId": "file123",
+ "activityData": {
+ "url": "url-to-file"
}
- ]
+ }
},
"Subscription": {
"title": "Subscription",
@@ -7182,6 +8172,13 @@
"type": "string",
"format": "date-time"
}
+ },
+ "required": ["kind", "subjectId", "createdAt"],
+ "additionalProperties": false,
+ "example": {
+ "kind": "thread",
+ "subjectId": "th_abc123",
+ "createdAt": "2022-07-13T14:32:50.697Z"
}
},
"UserSubscription": {
@@ -7198,333 +8195,422 @@
}
}
}
- ]
+ ],
+ "example": {
+ "kind": "thread",
+ "subjectId": "th_abc123",
+ "createdAt": "2022-07-13T14:32:50.697Z",
+ "userId": "alice"
+ }
},
- "AiCopilot": {
- "title": "AiCopilot",
- "oneOf": [
+ "AiCopilotBase": {
+ "type": "object",
+ "required": [
+ "type",
+ "id",
+ "name",
+ "systemPrompt",
+ "alwaysUseKnowledge",
+ "createdAt",
+ "updatedAt"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "copilot"
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "systemPrompt": {
+ "type": "string"
+ },
+ "knowledgePrompt": {
+ "type": "string"
+ },
+ "alwaysUseKnowledge": {
+ "type": "boolean"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "lastUsedAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "settings": {
+ "$ref": "#/components/schemas/AiCopilotProviderSettings"
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "type": "copilot",
+ "id": "cp_abc123",
+ "name": "My Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "alwaysUseKnowledge": true,
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "settings": {
+ "maxTokens": 4096,
+ "temperature": 0.7
+ }
+ }
+ },
+ "AiCopilotOpenAi": {
+ "title": "AiCopilotOpenAi",
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/AiCopilotBase"
+ },
{
"type": "object",
"properties": {
- "type": {
- "const": "copilot"
- },
- "id": {
- "type": "string"
- },
- "name": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "systemPrompt": {
- "type": "string"
- },
- "knowledgePrompt": {
- "type": "string"
- },
- "alwaysUseKnowledge": {
- "type": "boolean"
- },
"provider": {
+ "type": "string",
"const": "openai"
},
"providerModel": {
- "type": "string",
- "enum": [
- "o1",
- "o1-mini",
- "o3",
- "o3-mini",
- "o4-mini",
- "gpt-4.1",
- "gpt-4.1-mini",
- "gpt-4.1-nano",
- "gpt-4o",
- "gpt-4o-mini",
- "gpt-4-turbo",
- "gpt-4",
- "gpt-5",
- "gpt-5-mini",
- "gpt-5-nano",
- "gpt-5-chat-latest",
- "gpt-5.1",
- "gpt-5.1-mini",
- "gpt-5.1-chat-latest"
- ]
- },
- "providerOptions": {
- "type": "object",
- "properties": {
- "openai": {
- "type": "object",
- "properties": {
- "reasoningEffort": {
- "type": "string",
- "enum": ["low", "medium", "high"]
- },
- "webSearch": {
- "type": "object",
- "properties": {
- "allowedDomains": {
- "type": "array",
- "items": { "type": "string" }
- }
- }
- }
- }
- }
- }
- },
- "createdAt": {
- "type": "string",
- "format": "date-time"
- },
- "updatedAt": {
- "type": "string",
- "format": "date-time"
- },
- "lastUsedAt": {
- "type": "string",
- "format": "date-time"
- },
- "settings": {
- "$ref": "#/components/schemas/CopilotSettings"
+ "$ref": "#/components/schemas/OpenAiModel"
}
},
- "required": [
- "type",
- "id",
- "name",
- "systemPrompt",
- "provider",
- "providerModel",
- "createdAt",
- "updatedAt"
- ]
+ "required": ["provider", "providerModel"]
+ }
+ ],
+ "additionalProperties": false,
+ "example": {
+ "type": "copilot",
+ "id": "cp_abc123",
+ "name": "My Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "alwaysUseKnowledge": true,
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "provider": "openai",
+ "providerModel": "gpt-4o",
+ "settings": {
+ "maxTokens": 4096,
+ "temperature": 0.7
+ }
+ }
+ },
+ "AiCopilotAnthropic": {
+ "title": "AiCopilotAnthropic",
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/AiCopilotBase"
},
{
"type": "object",
"properties": {
- "type": {
- "const": "copilot"
- },
- "id": {
- "type": "string"
- },
- "name": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "systemPrompt": {
- "type": "string"
- },
- "knowledgePrompt": {
- "type": "string"
- },
- "alwaysUseKnowledge": {
- "type": "boolean"
- },
"provider": {
+ "type": "string",
"const": "anthropic"
},
"providerModel": {
- "type": "string",
- "enum": [
- "claude-sonnet-4-5-20250929",
- "claude-haiku-4-5-20251001",
- "claude-opus-4-1-20250805",
- "claude-4-opus-20250514",
- "claude-4-sonnet-20250514",
- "claude-3-7-sonnet-20250219",
- "claude-3-5-sonnet-latest",
- "claude-3-5-haiku-latest",
- "claude-3-opus-latest"
- ]
+ "$ref": "#/components/schemas/AnthropicModel"
},
"providerOptions": {
- "type": "object",
- "properties": {
- "anthropic": {
- "type": "object",
- "properties": {
- "thinking": {
- "oneOf": [
- {
- "type": "object",
- "properties": {
- "type": {
- "const": "enabled"
- },
- "budgetTokens": {
- "type": "number",
- "minimum": 0
- }
- },
- "required": ["type", "s"]
- },
- {
- "type": "object",
- "properties": {
- "type": {
- "const": "disabled"
- }
- },
- "required": ["type"]
- }
- ]
- },
- "webSearch": {
- "type": "object",
- "properties": {
- "allowedDomains": {
- "type": "array",
- "items": { "type": "string" }
- }
- }
- }
- }
- }
- }
- },
- "createdAt": {
- "type": "string",
- "format": "date-time"
- },
- "updatedAt": {
+ "$ref": "#/components/schemas/AnthropicProviderOptions"
+ }
+ },
+ "required": ["provider", "providerModel"]
+ }
+ ],
+ "additionalProperties": false,
+ "example": {
+ "type": "copilot",
+ "id": "cp_abc456",
+ "name": "My Anthropic Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "alwaysUseKnowledge": true,
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "provider": "anthropic",
+ "providerModel": "claude-3-5-sonnet-latest"
+ }
+ },
+ "AiCopilotGoogle": {
+ "title": "AiCopilotGoogle",
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/AiCopilotBase"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "provider": {
"type": "string",
- "format": "date-time"
+ "const": "google"
},
- "lastUsedAt": {
- "type": "string",
- "format": "date-time"
+ "providerModel": {
+ "$ref": "#/components/schemas/GoogleModel"
},
- "settings": {
- "$ref": "#/components/schemas/CopilotSettings"
+ "providerOptions": {
+ "$ref": "#/components/schemas/GoogleProviderOptions"
}
},
- "required": [
- "type",
- "id",
- "name",
- "systemPrompt",
- "provider",
- "providerModel",
- "createdAt",
- "updatedAt"
- ]
+ "required": ["provider", "providerModel"]
+ }
+ ],
+ "additionalProperties": false,
+ "example": {
+ "type": "copilot",
+ "id": "cp_abc789",
+ "name": "My Google Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "alwaysUseKnowledge": false,
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "provider": "google",
+ "providerModel": "gemini-2.5-flash"
+ }
+ },
+ "AiCopilotOpenAiCompatible": {
+ "title": "AiCopilotOpenAiCompatible",
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/AiCopilotBase"
},
{
"type": "object",
+ "required": [
+ "provider",
+ "providerModel",
+ "compatibleProviderName",
+ "providerBaseUrl"
+ ],
"properties": {
- "type": {
- "const": "copilot"
- },
- "id": {
- "type": "string"
- },
- "name": {
- "type": "string"
+ "provider": {
+ "type": "string",
+ "const": "openai-compatible"
},
- "description": {
+ "providerModel": {
"type": "string"
},
- "systemPrompt": {
+ "compatibleProviderName": {
"type": "string"
},
- "knowledgePrompt": {
+ "providerBaseUrl": {
"type": "string"
- },
- "alwaysUseKnowledge": {
- "type": "boolean"
- },
+ }
+ }
+ }
+ ],
+ "additionalProperties": false,
+ "example": {
+ "type": "copilot",
+ "id": "cp_compat1",
+ "name": "My Compatible Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "alwaysUseKnowledge": false,
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "provider": "openai-compatible",
+ "providerModel": "my-custom-model",
+ "compatibleProviderName": "my-provider",
+ "providerBaseUrl": "https://api.my-provider.com/v1"
+ }
+ },
+ "AiCopilot": {
+ "title": "AiCopilot",
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/AiCopilotOpenAi"
+ },
+ {
+ "$ref": "#/components/schemas/AiCopilotAnthropic"
+ },
+ {
+ "$ref": "#/components/schemas/AiCopilotGoogle"
+ },
+ {
+ "$ref": "#/components/schemas/AiCopilotOpenAiCompatible"
+ }
+ ],
+ "discriminator": {
+ "propertyName": "provider",
+ "mapping": {
+ "openai": "#/components/schemas/AiCopilotOpenAi",
+ "anthropic": "#/components/schemas/AiCopilotAnthropic",
+ "google": "#/components/schemas/AiCopilotGoogle",
+ "openai-compatible": "#/components/schemas/AiCopilotOpenAiCompatible"
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "type": "copilot",
+ "id": "cp_abc123",
+ "name": "My Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "alwaysUseKnowledge": true,
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "provider": "openai",
+ "providerModel": "gpt-4o",
+ "settings": {
+ "maxTokens": 4096,
+ "temperature": 0.7
+ }
+ }
+ },
+ "CreateAiCopilotOptionsBase": {
+ "type": "object",
+ "required": ["name", "systemPrompt", "providerApiKey"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "systemPrompt": {
+ "type": "string"
+ },
+ "knowledgePrompt": {
+ "type": "string"
+ },
+ "alwaysUseKnowledge": {
+ "type": "boolean"
+ },
+ "settings": {
+ "$ref": "#/components/schemas/AiCopilotProviderSettings"
+ },
+ "providerApiKey": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "name": "My Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "providerApiKey": "sk-...",
+ "alwaysUseKnowledge": true,
+ "settings": {
+ "maxTokens": 4096,
+ "temperature": 0.7
+ }
+ }
+ },
+ "CreateAiCopilotOptionsOpenAi": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/CreateAiCopilotOptionsBase"
+ },
+ {
+ "type": "object",
+ "required": ["provider", "providerModel"],
+ "properties": {
"provider": {
- "const": "google"
+ "type": "string",
+ "const": "openai"
},
"providerModel": {
- "type": "string",
- "enum": [
- "gemini-2.5-flash",
- "gemini-2.5-pro",
- "gemini-2.0-flash-001",
- "gemini-1.5-flash",
- "gemini-1.5-pro"
- ]
+ "$ref": "#/components/schemas/OpenAiModel"
},
"providerOptions": {
- "type": "object",
- "properties": {
- "google": {
- "type": "object",
- "properties": {
- "thinkingConfig": {
- "type": "object",
- "properties": {
- "thinkingBudget": {
- "type": "number",
- "minimum": 0
- }
- }
- }
- }
- }
- }
- },
- "createdAt": {
+ "$ref": "#/components/schemas/OpenAiProviderOptions"
+ }
+ },
+ "additionalProperties": false
+ }
+ ],
+ "additionalProperties": false,
+ "example": {
+ "name": "My Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "providerApiKey": "sk-...",
+ "provider": "openai",
+ "providerModel": "gpt-4o"
+ }
+ },
+ "CreateAiCopilotOptionsAnthropic": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/CreateAiCopilotOptionsBase"
+ },
+ {
+ "type": "object",
+ "required": ["provider", "providerModel"],
+ "properties": {
+ "provider": {
"type": "string",
- "format": "date-time"
+ "const": "anthropic"
},
- "updatedAt": {
- "type": "string",
- "format": "date-time"
+ "providerModel": {
+ "$ref": "#/components/schemas/AnthropicModel"
},
- "lastUsedAt": {
+ "providerOptions": {
+ "$ref": "#/components/schemas/AnthropicProviderOptions"
+ }
+ }
+ }
+ ],
+ "example": {
+ "name": "My Anthropic Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "providerApiKey": "sk-ant-...",
+ "provider": "anthropic",
+ "providerModel": "claude-3-5-sonnet-latest"
+ }
+ },
+ "CreateAiCopilotOptionsGoogle": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/CreateAiCopilotOptionsBase"
+ },
+ {
+ "type": "object",
+ "required": ["provider", "providerModel"],
+ "properties": {
+ "provider": {
"type": "string",
- "format": "date-time"
+ "const": "google"
},
- "settings": {
- "$ref": "#/components/schemas/CopilotSettings"
+ "providerModel": {
+ "$ref": "#/components/schemas/GoogleModel"
+ },
+ "providerOptions": {
+ "$ref": "#/components/schemas/GoogleProviderOptions"
}
- },
- "required": [
- "type",
- "id",
- "name",
- "systemPrompt",
- "provider",
- "providerModel",
- "createdAt",
- "updatedAt"
- ]
+ }
+ }
+ ],
+ "example": {
+ "name": "My Google Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "providerApiKey": "AIza...",
+ "provider": "google",
+ "providerModel": "gemini-2.5-flash"
+ }
+ },
+ "CreateAiCopilotOptionsOpenAiCompatible": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/CreateAiCopilotOptionsBase"
},
{
"type": "object",
+ "required": [
+ "provider",
+ "providerModel",
+ "compatibleProviderName",
+ "providerBaseUrl"
+ ],
"properties": {
- "type": {
- "const": "copilot"
- },
- "id": {
- "type": "string"
- },
- "name": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "systemPrompt": {
- "type": "string"
- },
- "knowledgePrompt": {
- "type": "string"
- },
- "alwaysUseKnowledge": {
- "type": "boolean"
- },
"provider": {
+ "type": "string",
"const": "openai-compatible"
},
"providerModel": {
@@ -7534,170 +8620,121 @@
"type": "string"
},
"providerBaseUrl": {
- "type": "string",
- "format": "uri"
- },
- "createdAt": {
- "type": "string",
- "format": "date-time"
- },
- "updatedAt": {
- "type": "string",
- "format": "date-time"
- },
- "lastUsedAt": {
- "type": "string",
- "format": "date-time"
- },
- "settings": {
- "$ref": "#/components/schemas/CopilotSettings"
+ "type": "string"
}
- },
- "required": [
- "type",
- "id",
- "name",
- "systemPrompt",
- "provider",
- "providerModel",
- "compatibleProviderName",
- "providerBaseUrl",
- "createdAt",
- "updatedAt"
- ]
+ }
+ }
+ ],
+ "example": {
+ "name": "My Compatible Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "providerApiKey": "sk-...",
+ "provider": "openai-compatible",
+ "providerModel": "my-custom-model",
+ "compatibleProviderName": "my-provider",
+ "providerBaseUrl": "https://api.my-provider.com/v1"
+ }
+ },
+ "CreateAiCopilotRequestBody": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/CreateAiCopilotOptionsOpenAi"
+ },
+ {
+ "$ref": "#/components/schemas/CreateAiCopilotOptionsAnthropic"
+ },
+ {
+ "$ref": "#/components/schemas/CreateAiCopilotOptionsGoogle"
+ },
+ {
+ "$ref": "#/components/schemas/CreateAiCopilotOptionsOpenAiCompatible"
}
],
"discriminator": {
- "propertyName": "provider"
+ "propertyName": "provider",
+ "mapping": {
+ "openai": "#/components/schemas/CreateAiCopilotOptionsOpenAi",
+ "anthropic": "#/components/schemas/CreateAiCopilotOptionsAnthropic",
+ "google": "#/components/schemas/CreateAiCopilotOptionsGoogle",
+ "openai-compatible": "#/components/schemas/CreateAiCopilotOptionsOpenAiCompatible"
+ }
+ },
+ "example": {
+ "name": "My Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "providerApiKey": "sk-...",
+ "provider": "openai",
+ "providerModel": "gpt-4o"
}
},
- "CreateAiCopilot": {
- "title": "CreateAiCopilot",
+ "UpdateAiCopilotRequestBody": {
+ "title": "UpdateAiCopilotRequestBody",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
- "type": "string",
- "nullable": true
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
"systemPrompt": {
"type": "string"
},
"knowledgePrompt": {
- "type": "string",
- "nullable": true
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
"alwaysUseKnowledge": {
- "type": "boolean",
- "nullable": true
+ "type": "boolean"
+ },
+ "settings": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/AiCopilotProviderSettings"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
"providerApiKey": {
"type": "string"
},
+ "provider": {
+ "type": "string",
+ "enum": ["openai", "anthropic", "google", "openai-compatible"]
+ },
"providerModel": {
"type": "string"
},
"providerOptions": {
"oneOf": [
{
- "type": "object",
- "properties": {
- "openai": {
- "type": "object",
- "properties": {
- "reasoningEffort": {
- "type": "string",
- "enum": ["low", "medium", "high"]
- },
- "webSearch": {
- "type": "object",
- "properties": {
- "allowedDomains": {
- "type": "array",
- "items": { "type": "string" }
- }
- }
- }
- }
- }
- }
+ "$ref": "#/components/schemas/OpenAiProviderOptions"
},
{
- "type": "object",
- "properties": {
- "anthropic": {
- "type": "object",
- "properties": {
- "thinking": {
- "oneOf": [
- {
- "type": "object",
- "properties": {
- "type": {
- "const": "enabled"
- },
- "budgetTokens": {
- "type": "number",
- "minimum": 0
- }
- },
- "required": ["type", "budgetTokens"]
- },
- {
- "type": "object",
- "properties": {
- "type": {
- "const": "disabled"
- }
- },
- "required": ["type"]
- }
- ]
- },
- "webSearch": {
- "type": "object",
- "properties": {
- "allowedDomains": {
- "type": "array",
- "items": { "type": "string" }
- }
- }
- }
- }
- }
- }
+ "$ref": "#/components/schemas/AnthropicProviderOptions"
},
{
- "type": "object",
- "properties": {
- "google": {
- "type": "object",
- "properties": {
- "thinkingConfig": {
- "type": "object",
- "properties": {
- "thinkingBudget": {
- "type": "number",
- "minimum": 0
- }
- }
- }
- }
- }
- }
+ "$ref": "#/components/schemas/GoogleProviderOptions"
+ },
+ {
+ "type": "null"
}
- ],
- "nullable": true
- },
- "settings": {
- "$ref": "#/components/schemas/CopilotSettings",
- "nullable": true
- },
- "provider": {
- "type": "string",
- "enum": ["openai", "anthropic", "google", "openai-compatible"]
+ ]
},
"compatibleProviderName": {
"type": "string"
@@ -7707,193 +8744,251 @@
"format": "uri"
}
},
- "required": [
- "name",
- "systemPrompt",
- "provider",
- "providerApiKey",
- "providerModel"
- ]
+ "additionalProperties": false,
+ "example": {
+ "name": "Updated Copilot",
+ "systemPrompt": "You are an updated helpful assistant.",
+ "providerModel": "gpt-4o",
+ "settings": {
+ "maxTokens": 8192
+ }
+ }
},
- "UpdateAiCopilot": {
- "title": "UpdateAiCopilot",
+ "AiCopilotProviderSettings": {
+ "title": "AiCopilotProviderSettings",
"type": "object",
"properties": {
- "name": {
- "type": "string"
+ "maxTokens": {
+ "type": "integer"
},
- "description": {
- "type": "string",
- "nullable": true
+ "temperature": {
+ "type": "number"
},
- "systemPrompt": {
- "type": "string"
+ "topP": {
+ "type": "number"
},
- "knowledgePrompt": {
- "type": "string",
- "nullable": true
+ "topK": {
+ "type": "number"
},
- "alwaysUseKnowledge": {
- "type": "boolean",
- "nullable": true
+ "frequencyPenalty": {
+ "type": "number"
},
- "providerApiKey": {
- "type": "string"
+ "presencePenalty": {
+ "type": "number"
},
- "providerModel": {
- "type": "string"
+ "stopSequences": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
},
- "providerOptions": {
- "oneOf": [
- {
+ "seed": {
+ "type": "integer"
+ },
+ "maxRetries": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "maxTokens": 4096,
+ "temperature": 0.7,
+ "topP": 0.9
+ }
+ },
+ "OpenAiModel": {
+ "title": "OpenAiModel",
+ "type": "string",
+ "enum": [
+ "o1",
+ "o1-mini",
+ "o3",
+ "o3-mini",
+ "o4-mini",
+ "gpt-4.1",
+ "gpt-4.1-mini",
+ "gpt-4.1-nano",
+ "gpt-4o",
+ "gpt-4o-mini",
+ "gpt-4-turbo",
+ "gpt-4",
+ "gpt-5",
+ "gpt-5-mini",
+ "gpt-5-nano",
+ "gpt-5-chat-latest",
+ "gpt-5.1",
+ "gpt-5.1-mini",
+ "gpt-5.1-chat-latest"
+ ],
+ "example": "gpt-4o"
+ },
+ "AnthropicModel": {
+ "title": "AnthropicModel",
+ "type": "string",
+ "enum": [
+ "claude-sonnet-4-5-20250929",
+ "claude-haiku-4-5-20251001",
+ "claude-opus-4-1-20250805",
+ "claude-4-opus-20250514",
+ "claude-4-sonnet-20250514",
+ "claude-3-7-sonnet-20250219",
+ "claude-3-5-sonnet-latest",
+ "claude-3-5-haiku-latest",
+ "claude-3-opus-latest"
+ ],
+ "example": "claude-3-5-sonnet-latest"
+ },
+ "GoogleModel": {
+ "title": "GoogleModel",
+ "type": "string",
+ "enum": [
+ "gemini-2.5-flash",
+ "gemini-2.5-pro",
+ "gemini-2.0-flash-001",
+ "gemini-1.5-flash",
+ "gemini-1.5-pro"
+ ],
+ "example": "gemini-2.5-flash"
+ },
+ "OpenAiProviderOptions": {
+ "type": "object",
+ "required": ["openai"],
+ "properties": {
+ "openai": {
+ "type": "object",
+ "properties": {
+ "reasoningEffort": {
+ "type": "string",
+ "enum": ["low", "medium", "high"]
+ },
+ "webSearch": {
"type": "object",
"properties": {
- "openai": {
+ "allowedDomains": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "openai": {
+ "reasoningEffort": "medium"
+ }
+ }
+ },
+ "AnthropicProviderOptions": {
+ "type": "object",
+ "required": ["anthropic"],
+ "properties": {
+ "anthropic": {
+ "type": "object",
+ "properties": {
+ "thinking": {
+ "oneOf": [
+ {
+ "title": "AnthropicThinkingEnabled",
"type": "object",
+ "required": ["type", "budgetTokens"],
"properties": {
- "reasoningEffort": {
+ "type": {
"type": "string",
- "enum": ["low", "medium", "high"]
+ "const": "enabled"
},
- "webSearch": {
- "type": "object",
- "properties": {
- "allowedDomains": {
- "type": "array",
- "items": { "type": "string" }
- }
- }
+ "budgetTokens": {
+ "type": "integer"
}
- }
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "anthropic": {
+ },
+ "additionalProperties": false
+ },
+ {
+ "title": "AnthropicThinkingDisabled",
"type": "object",
+ "required": ["type"],
"properties": {
- "thinking": {
- "oneOf": [
- {
- "type": "object",
- "properties": {
- "type": {
- "const": "enabled"
- },
- "budgetTokens": {
- "type": "number",
- "minimum": 0
- }
- },
- "required": ["type", "budgetTokens"]
- },
- {
- "type": "object",
- "properties": {
- "type": {
- "const": "disabled"
- }
- },
- "required": ["type"]
- }
- ]
- },
- "webSearch": {
- "type": "object",
- "properties": {
- "allowedDomains": {
- "type": "array",
- "items": { "type": "string" }
- }
- }
+ "type": {
+ "type": "string",
+ "const": "disabled"
}
- }
+ },
+ "additionalProperties": false
}
- }
+ ]
},
- {
+ "webSearch": {
+ "title": "AnthropicWebSearch",
"type": "object",
"properties": {
- "google": {
- "type": "object",
- "properties": {
- "thinkingConfig": {
- "type": "object",
- "properties": {
- "thinkingBudget": {
- "type": "number",
- "minimum": 0
- }
- }
- }
+ "allowedDomains": {
+ "type": "array",
+ "items": {
+ "type": "string"
}
}
}
}
- ],
- "nullable": true
- },
- "settings": {
- "$ref": "#/components/schemas/CopilotSettings",
- "nullable": true
- },
- "provider": {
- "type": "string",
- "enum": ["openai", "anthropic", "google", "openai-compatible"]
- },
- "compatibleProviderName": {
- "type": "string"
- },
- "providerBaseUrl": {
- "type": "string",
- "format": "uri"
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "anthropic": {
+ "thinking": {
+ "type": "enabled",
+ "budgetTokens": 10000
+ }
}
}
},
- "CopilotSettings": {
- "title": "CopilotSettings",
+ "GoogleProviderOptions": {
+ "title": "GoogleProviderOptions",
"type": "object",
+ "required": ["google"],
"properties": {
- "maxTokens": {
- "type": "number"
- },
- "temperature": {
- "type": "number"
- },
- "topP": {
- "type": "number"
- },
- "topK": {
- "type": "number"
- },
- "frequencyPenalty": {
- "type": "number"
- },
- "presencePenalty": {
- "type": "number"
- },
- "stopSequences": {
- "type": "array",
- "items": {
- "type": "string"
+ "google": {
+ "type": "object",
+ "properties": {
+ "thinkingConfig": {
+ "type": "object",
+ "properties": {
+ "thinkingBudget": {
+ "type": "integer"
+ }
+ }
+ }
+ }
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "google": {
+ "thinkingConfig": {
+ "thinkingBudget": 10000
}
- },
- "seed": {
- "type": "number"
- },
- "maxRetries": {
- "type": "number"
}
}
},
- "GetAiCopilots": {
- "title": "GetAiCopilots",
+ "GetAiCopilotsResponse": {
+ "title": "GetAiCopilotsResponse",
"type": "object",
"properties": {
"nextCursor": {
- "type": "string"
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
"data": {
"type": "array",
@@ -7901,42 +8996,78 @@
"$ref": "#/components/schemas/AiCopilot"
}
}
+ },
+ "required": ["nextCursor", "data"],
+ "additionalProperties": false,
+ "example": {
+ "nextCursor": null,
+ "data": [
+ {
+ "type": "copilot",
+ "id": "cp_abc123",
+ "name": "My Copilot",
+ "systemPrompt": "You are a helpful assistant.",
+ "alwaysUseKnowledge": true,
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "provider": "openai",
+ "providerModel": "gpt-4o"
+ }
+ ]
}
},
- "KnowledgeSource": {
- "title": "KnowledgeSource",
+ "KnowledgeSourceBase": {
"type": "object",
- "oneOf": [
+ "required": ["id", "createdAt", "updatedAt", "lastIndexedAt", "status"],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "lastIndexedAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["ingesting", "ready", "error"]
+ },
+ "errorMessage": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "example": {
+ "id": "ks_abc123",
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "lastIndexedAt": "2024-06-01T12:00:00.000Z",
+ "status": "ready"
+ }
+ },
+ "KnowledgeSourceWebSource": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/KnowledgeSourceBase"
+ },
{
- "title": "WebKnowledgeSource",
+ "type": "object",
+ "required": ["type", "link", "status"],
"properties": {
"type": {
- "const": "ai-knowledge-web-source"
- },
- "id": {
- "type": "string"
- },
- "createdAt": {
- "type": "string",
- "format": "date-time"
- },
- "updatedAt": {
- "type": "string",
- "format": "date-time"
- },
- "lastIndexedAt": {
- "type": "string",
- "format": "date-time"
- },
- "status": {
"type": "string",
- "enum": ["ingesting", "ready", "error"]
- },
- "errorMessage": {
- "type": "string"
+ "const": "ai-knowledge-web-source"
},
"link": {
"type": "object",
+ "required": ["url", "type"],
"properties": {
"url": {
"type": "string"
@@ -7948,37 +9079,38 @@
}
}
}
+ }
+ ],
+ "additionalProperties": false,
+ "example": {
+ "id": "ks_web123",
+ "type": "ai-knowledge-web-source",
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "lastIndexedAt": "2024-06-01T12:00:00.000Z",
+ "status": "ready",
+ "link": {
+ "url": "https://docs.example.com",
+ "type": "crawl"
+ }
+ }
+ },
+ "KnowledgeSourceFileSource": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/KnowledgeSourceBase"
},
{
- "title": "FileKnowledgeSource",
+ "type": "object",
+ "required": ["type", "file"],
"properties": {
"type": {
- "const": "ai-knowledge-file-source"
- },
- "id": {
- "type": "string"
- },
- "createdAt": {
- "type": "string",
- "format": "date-time"
- },
- "updatedAt": {
- "type": "string",
- "format": "date-time"
- },
- "lastIndexedAt": {
- "type": "string",
- "format": "date-time"
- },
- "status": {
"type": "string",
- "enum": ["ingesting", "ready", "error"]
- },
- "errorMessage": {
- "type": "string"
+ "const": "ai-knowledge-file-source"
},
"file": {
"type": "object",
+ "required": ["name", "mimeType"],
"properties": {
"name": {
"type": "string"
@@ -7990,38 +9122,142 @@
}
}
}
- ]
+ ],
+ "example": {
+ "id": "ks_file123",
+ "type": "ai-knowledge-file-source",
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "lastIndexedAt": "2024-06-01T12:00:00.000Z",
+ "status": "ready",
+ "file": {
+ "name": "document.pdf",
+ "mimeType": "application/pdf"
+ }
+ }
+ },
+ "KnowledgeSource": {
+ "type": "object",
+ "title": "KnowledgeSource",
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/KnowledgeSourceWebSource"
+ },
+ {
+ "$ref": "#/components/schemas/KnowledgeSourceFileSource"
+ }
+ ],
+ "discriminator": {
+ "propertyName": "type"
+ },
+ "additionalProperties": false,
+ "example": {
+ "id": "ks_web123",
+ "type": "ai-knowledge-web-source",
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "lastIndexedAt": "2024-06-01T12:00:00.000Z",
+ "status": "ready",
+ "link": {
+ "url": "https://docs.example.com",
+ "type": "crawl"
+ }
+ }
+ },
+ "GetKnowledgeSourcesResponse": {
+ "title": "GetKnowledgeSourcesResponse",
+ "type": "object",
+ "properties": {
+ "nextCursor": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/KnowledgeSource"
+ }
+ }
+ },
+ "required": ["nextCursor", "data"],
+ "additionalProperties": false,
+ "example": {
+ "nextCursor": null,
+ "data": [
+ {
+ "id": "ks_abc123",
+ "type": "ai-knowledge-web-source",
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "updatedAt": "2024-06-01T12:00:00.000Z",
+ "lastIndexedAt": "2024-06-01T12:00:00.000Z",
+ "status": "ready",
+ "link": {
+ "url": "https://docs.example.com",
+ "type": "crawl"
+ }
+ }
+ ]
+ }
},
- "CreateWebKnowledgeSource": {
- "title": "CreateWebKnowledgeSource",
+ "CreateWebKnowledgeSourceRequestBody": {
+ "title": "CreateWebKnowledgeSourceRequestBody",
"type": "object",
"properties": {
"copilotId": {
"type": "string"
},
"url": {
- "type": "string"
+ "type": "string",
+ "format": "uri"
},
"type": {
"type": "string",
"enum": ["individual_link", "crawl", "sitemap"]
}
},
- "required": ["copilotId", "url", "type"]
+ "required": ["copilotId", "url", "type"],
+ "example": {
+ "copilotId": "cp_abc123",
+ "url": "https://docs.example.com",
+ "type": "crawl"
+ }
},
- "GetKnowledgeSources": {
- "title": "GetKnowledgeSources",
+ "CreateWebKnowledgeSourceResponse": {
+ "title": "CreateWebKnowledgeSourceResponse",
"type": "object",
"properties": {
- "nextCursor": {
+ "id": {
+ "type": "string"
+ }
+ },
+ "required": ["id"],
+ "additionalProperties": false,
+ "example": {
+ "id": "ks_abc123"
+ }
+ },
+ "GetFileKnowledgeSourceMarkdownResponse": {
+ "title": "GetFileKnowledgeSourceMarkdownResponse",
+ "type": "object",
+ "properties": {
+ "id": {
"type": "string"
},
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/KnowledgeSource"
- }
+ "content": {
+ "type": "string"
}
+ },
+ "required": ["id", "content"],
+ "additionalProperties": false,
+ "example": {
+ "id": "ks_abc123",
+ "content": "# Document Title\n\nThis is the content of the uploaded file."
}
},
"WebKnowledgeSourceLink": {
@@ -8047,14 +9283,29 @@
"format": "date-time"
}
},
- "required": ["id", "url", "status", "createdAt", "lastIndexedAt"]
+ "required": ["id", "url", "status", "createdAt", "lastIndexedAt"],
+ "additionalProperties": false,
+ "example": {
+ "id": "ksl_abc123",
+ "url": "https://docs.example.com/getting-started",
+ "status": "ready",
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "lastIndexedAt": "2024-06-01T12:00:00.000Z"
+ }
},
- "GetWebKnowledgeSourceLinks": {
- "title": "GetWebKnowledgeSourceLinks",
+ "GetWebKnowledgeSourceLinksResponse": {
+ "title": "GetWebKnowledgeSourceLinksResponse",
"type": "object",
"properties": {
"nextCursor": {
- "type": "string"
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
"data": {
"type": "array",
@@ -8062,6 +9313,20 @@
"$ref": "#/components/schemas/WebKnowledgeSourceLink"
}
}
+ },
+ "required": ["nextCursor", "data"],
+ "additionalProperties": false,
+ "example": {
+ "nextCursor": null,
+ "data": [
+ {
+ "id": "ksl_abc123",
+ "url": "https://docs.example.com/getting-started",
+ "status": "ready",
+ "createdAt": "2024-06-01T12:00:00.000Z",
+ "lastIndexedAt": "2024-06-01T12:00:00.000Z"
+ }
+ ]
}
},
"Group": {
@@ -8070,7 +9335,7 @@
"properties": {
"type": {
"type": "string",
- "enum": ["group"]
+ "const": "group"
},
"id": {
"type": "string"
@@ -8109,7 +9374,27 @@
"updatedAt",
"scopes",
"members"
- ]
+ ],
+ "example": {
+ "type": "group",
+ "id": "engineering",
+ "organizationId": "org_123456789",
+ "createdAt": "2024-01-15T10:30:00.000Z",
+ "updatedAt": "2024-01-15T10:30:00.000Z",
+ "scopes": {
+ "mention": true
+ },
+ "members": [
+ {
+ "id": "alice",
+ "addedAt": "2024-01-15T10:30:00.000Z"
+ },
+ {
+ "id": "bob",
+ "addedAt": "2024-01-16T09:00:00.000Z"
+ }
+ ]
+ }
},
"GroupMember": {
"title": "GroupMember",
@@ -8123,13 +9408,17 @@
"format": "date-time"
}
},
- "required": ["id", "addedAt"]
+ "required": ["id", "addedAt"],
+ "example": {
+ "id": "alice",
+ "addedAt": "2024-01-15T10:30:00.000Z"
+ }
},
- "CreateGroup": {
- "title": "CreateGroup",
+ "CreateGroupRequestBody": {
+ "title": "CreateGroupRequestBody",
"type": "object",
"properties": {
- "groupId": {
+ "id": {
"type": "string"
},
"memberIds": {
@@ -8150,10 +9439,19 @@
}
}
},
- "required": ["groupId"]
+ "required": ["id"],
+ "additionalProperties": false,
+ "example": {
+ "id": "engineering",
+ "memberIds": ["alice", "bob"],
+ "organizationId": "org_123456789",
+ "scopes": {
+ "mention": true
+ }
+ }
},
- "AddGroupMembers": {
- "title": "AddGroupMembers",
+ "AddGroupMembersRequestBody": {
+ "title": "AddGroupMembersRequestBody",
"type": "object",
"properties": {
"memberIds": {
@@ -8163,10 +9461,13 @@
}
}
},
- "required": ["memberIds"]
+ "required": ["memberIds"],
+ "example": {
+ "memberIds": ["charlie", "dave"]
+ }
},
- "RemoveGroupMembers": {
- "title": "RemoveGroupMembers",
+ "RemoveGroupMembersRequestBody": {
+ "title": "RemoveGroupMembersRequestBody",
"type": "object",
"properties": {
"memberIds": {
@@ -8176,10 +9477,13 @@
}
}
},
- "required": ["memberIds"]
+ "required": ["memberIds"],
+ "example": {
+ "memberIds": ["charlie"]
+ }
},
- "GetGroups": {
- "title": "GetGroups",
+ "GetGroupsResponse": {
+ "title": "GetGroupsResponse",
"type": "object",
"properties": {
"data": {
@@ -8189,12 +9493,42 @@
}
},
"nextCursor": {
- "type": "string"
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "example": {
+ "data": [
+ {
+ "type": "group",
+ "id": "engineering",
+ "organizationId": "org_123456789",
+ "createdAt": "2024-01-15T10:30:00.000Z",
+ "updatedAt": "2024-01-15T10:30:00.000Z",
+ "scopes": {
+ "mention": true
+ },
+ "members": [
+ {
+ "id": "alice",
+ "addedAt": "2024-01-15T10:30:00.000Z"
+ }
+ ]
+ }
+ ],
+ "nextCursor": null
}
},
- "GetUserGroups": {
- "title": "GetUserGroups",
+ "GetUserGroupsResponse": {
+ "title": "GetUserGroupsResponse",
"type": "object",
"properties": {
"data": {
@@ -8204,19 +9538,51 @@
}
},
"nextCursor": {
- "type": "string"
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "example": {
+ "data": [
+ {
+ "type": "group",
+ "id": "engineering",
+ "organizationId": "org_123456789",
+ "createdAt": "2024-01-15T10:30:00.000Z",
+ "updatedAt": "2024-01-15T10:30:00.000Z",
+ "scopes": {
+ "mention": true
+ },
+ "members": [
+ {
+ "id": "alice",
+ "addedAt": "2024-01-15T10:30:00.000Z"
+ }
+ ]
+ }
+ ],
+ "nextCursor": null
}
},
"ManagementProjectType": {
"title": "ManagementProjectType",
"type": "string",
- "enum": ["dev", "prod"]
+ "enum": ["dev", "prod"],
+ "example": "dev"
},
"ManagementProjectRegion": {
"title": "ManagementProjectRegion",
"type": "string",
- "enum": ["earth", "eu", "fedramp"]
+ "enum": ["earth", "eu", "fedramp"],
+ "example": "earth"
},
"ManagementProjectVersionCreationTimeout": {
"title": "ManagementProjectVersionCreationTimeout",
@@ -8227,11 +9593,12 @@
"enum": [false]
},
{
- "type": "number",
+ "type": "integer",
"minimum": 30,
"maximum": 300
}
- ]
+ ],
+ "example": false
},
"ManagementProjectPublicKey": {
"title": "ManagementProjectPublicKey",
@@ -8248,7 +9615,12 @@
"type": "string"
}
},
- "required": ["activated", "createdAt", "value"]
+ "required": ["activated", "createdAt", "value"],
+ "example": {
+ "activated": true,
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "value": "pk_dev_123"
+ }
},
"ManagementProjectSecretKey": {
"title": "ManagementProjectSecretKey",
@@ -8262,7 +9634,11 @@
"type": "string"
}
},
- "required": ["createdAt", "value"]
+ "required": ["createdAt", "value"],
+ "example": {
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "value": "sk_dev_123"
+ }
},
"ManagementProject": {
"title": "ManagementProject",
@@ -8292,8 +9668,14 @@
"$ref": "#/components/schemas/ManagementProjectPublicKey"
},
"secretKey": {
- "$ref": "#/components/schemas/ManagementProjectSecretKey",
- "nullable": true
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ManagementProjectSecretKey"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
"region": {
"$ref": "#/components/schemas/ManagementProjectRegion"
@@ -8313,10 +9695,77 @@
"secretKey",
"region",
"versionCreationTimeout"
- ]
+ ],
+ "example": {
+ "id": "683d49ed6b4d1cec5a597b13",
+ "teamId": "team_123",
+ "type": "dev",
+ "name": "My Project",
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "updatedAt": "2024-09-03T12:34:56.000Z",
+ "publicKey": {
+ "activated": true,
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "value": "pk_dev_123"
+ },
+ "secretKey": {
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "value": "sk_dev_123"
+ },
+ "region": "earth",
+ "versionCreationTimeout": false
+ }
+ },
+ "GetManagementProjectsResponse": {
+ "title": "GetManagementProjectsResponse",
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ManagementProject"
+ }
+ },
+ "nextCursor": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": ["projects", "nextCursor"],
+ "additionalProperties": false,
+ "example": {
+ "data": [
+ {
+ "id": "683d49ed6b4d1cec5a597b13",
+ "teamId": "team_123",
+ "type": "dev",
+ "name": "My Project",
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "updatedAt": "2024-09-03T12:34:56.000Z",
+ "publicKey": {
+ "activated": true,
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "value": "pk_dev_123"
+ },
+ "secretKey": {
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "value": "sk_dev_123"
+ },
+ "region": "earth",
+ "versionCreationTimeout": false
+ }
+ ],
+ "nextCursor": null
+ }
},
- "CreateManagementProject": {
- "title": "CreateManagementProject",
+ "CreateManagementProjectRequestBody": {
+ "title": "CreateManagementProjectRequestBody",
"type": "object",
"properties": {
"name": {
@@ -8330,10 +9779,15 @@
"default": false
}
},
- "required": ["type"]
+ "required": ["type"],
+ "example": {
+ "name": "My Project",
+ "type": "dev",
+ "versionCreationTimeout": false
+ }
},
- "UpdateManagementProject": {
- "title": "UpdateManagementProject",
+ "UpdateManagementProjectRequestBody": {
+ "title": "UpdateManagementProjectRequestBody",
"type": "object",
"properties": {
"name": {
@@ -8342,10 +9796,32 @@
"versionCreationTimeout": {
"$ref": "#/components/schemas/ManagementProjectVersionCreationTimeout"
}
+ },
+ "example": {
+ "name": "Updated Project Name",
+ "versionCreationTimeout": 60
+ }
+ },
+ "RollProjectPublicApiKeyResponse": {
+ "title": "RollProjectPublicApiKeyResponse",
+ "type": "object",
+ "properties": {
+ "publicKey": {
+ "$ref": "#/components/schemas/ManagementProjectPublicKey"
+ }
+ },
+ "required": ["publicKey"],
+ "additionalProperties": false,
+ "example": {
+ "publicKey": {
+ "activated": true,
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "value": "pk_dev_123"
+ }
}
},
- "ManagementProjectKeyRollRequest": {
- "title": "ManagementProjectKeyRollRequest",
+ "RollProjectPublicApiKeyRequestBody": {
+ "title": "RollProjectPublicApiKeyRequestBody",
"type": "object",
"properties": {
"expirationIn": {
@@ -8367,53 +9843,57 @@
],
"default": "now"
}
+ },
+ "additionalProperties": false,
+ "example": {
+ "expirationIn": "1 hour"
}
},
- "ManagementProjectsResponse": {
- "title": "ManagementProjectsResponse",
+ "RollProjectSecretApiKeyRequestBody": {
+ "title": "RollProjectSecretApiKeyRequestBody",
"type": "object",
"properties": {
- "projects": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/ManagementProject"
- }
- },
- "nextCursor": {
+ "expirationIn": {
"type": "string",
- "nullable": true
- }
- }
- },
- "ManagementProjectResponse": {
- "title": "ManagementProjectResponse",
- "type": "object",
- "properties": {
- "project": {
- "$ref": "#/components/schemas/ManagementProject"
- }
- },
- "required": ["project"]
- },
- "ManagementProjectPublicKeyResponse": {
- "title": "ManagementProjectPublicKeyResponse",
- "type": "object",
- "properties": {
- "publicKey": {
- "$ref": "#/components/schemas/ManagementProjectPublicKey"
+ "enum": [
+ "now",
+ "1h",
+ "1hour",
+ "1 hour",
+ "24hrs",
+ "24hours",
+ "24 hours",
+ "3d",
+ "3days",
+ "3 days",
+ "7d",
+ "7days",
+ "7 days"
+ ],
+ "default": "now"
}
},
- "required": ["publicKey"]
+ "additionalProperties": false,
+ "example": {
+ "expirationIn": "3 days"
+ }
},
- "ManagementProjectSecretKeyResponse": {
- "title": "ManagementProjectSecretKeyResponse",
+ "RollProjectSecretApiKeyResponse": {
+ "title": "ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse",
"type": "object",
"properties": {
"secretKey": {
"$ref": "#/components/schemas/ManagementProjectSecretKey"
}
},
- "required": ["secretKey"]
+ "required": ["secretKey"],
+ "additionalProperties": false,
+ "example": {
+ "secretKey": {
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "value": "sk_dev_123"
+ }
+ }
},
"ManagementWebhookEvent": {
"title": "ManagementWebhookEvent",
@@ -8437,7 +9917,8 @@
"notification",
"threadMarkedAsResolved",
"threadMarkedAsUnresolved"
- ]
+ ],
+ "example": "storageUpdated"
},
"ManagementWebhookSecret": {
"title": "ManagementWebhookSecret",
@@ -8447,7 +9928,10 @@
"type": "string"
}
},
- "required": ["value"]
+ "required": ["value"],
+ "example": {
+ "value": "whsec_abc123"
+ }
},
"ManagementWebhook": {
"title": "ManagementWebhook",
@@ -8472,7 +9956,7 @@
"type": "boolean"
},
"rateLimit": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 65534
},
@@ -8483,20 +9967,23 @@
}
},
"secret": {
- "$ref": "#/components/schemas/ManagementWebhookSecret",
- "nullable": true
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ManagementWebhookSecret"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
"additionalHeaders": {
- "type": "object",
- "additionalProperties": {
- "type": "string"
- }
+ "$ref": "#/components/schemas/ManagementWebhookAdditionalHeaders"
},
"storageUpdatedThrottleSeconds": {
- "type": "number"
+ "type": "integer"
},
"yDocUpdatedThrottleSeconds": {
- "type": "number"
+ "type": "integer"
}
},
"required": [
@@ -8509,10 +9996,24 @@
"secret",
"storageUpdatedThrottleSeconds",
"yDocUpdatedThrottleSeconds"
- ]
+ ],
+ "additionalProperties": false,
+ "example": {
+ "id": "wh_abc123",
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "updatedAt": "2024-09-03T12:34:56.000Z",
+ "url": "https://example.com/webhooks",
+ "disabled": false,
+ "subscribedEvents": ["storageUpdated", "userEntered"],
+ "secret": {
+ "value": "whsec_abc123"
+ },
+ "storageUpdatedThrottleSeconds": 10,
+ "yDocUpdatedThrottleSeconds": 10
+ }
},
- "CreateManagementWebhook": {
- "title": "CreateManagementWebhook",
+ "CreateManagementWebhookRequestBody": {
+ "title": "CreateManagementWebhookRequestBody",
"type": "object",
"properties": {
"url": {
@@ -8526,7 +10027,7 @@
}
},
"rateLimit": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 65534
},
@@ -8537,16 +10038,23 @@
}
},
"storageUpdatedThrottleSeconds": {
- "type": "number"
+ "type": "integer"
},
"yDocUpdatedThrottleSeconds": {
- "type": "number"
+ "type": "integer"
}
},
- "required": ["url", "subscribedEvents"]
+ "required": ["url", "subscribedEvents"],
+ "example": {
+ "url": "https://example.com/webhooks",
+ "subscribedEvents": ["storageUpdated", "userEntered"],
+ "rateLimit": 100,
+ "storageUpdatedThrottleSeconds": 10,
+ "yDocUpdatedThrottleSeconds": 10
+ }
},
- "UpdateManagementWebhook": {
- "title": "UpdateManagementWebhook",
+ "UpdateManagementWebhookRequestBody": {
+ "title": "UpdateManagementWebhookRequestBody",
"type": "object",
"properties": {
"url": {
@@ -8560,33 +10068,102 @@
}
},
"rateLimit": {
- "type": "number",
+ "type": "integer",
"minimum": 1,
"maximum": 65534
},
"storageUpdatedThrottleSeconds": {
- "type": "number"
+ "type": "integer"
},
"yDocUpdatedThrottleSeconds": {
- "type": "number"
+ "type": "integer"
},
"disabled": {
"type": "boolean"
}
+ },
+ "example": {
+ "url": "https://example.com/webhooks",
+ "subscribedEvents": ["storageUpdated", "userEntered"],
+ "rateLimit": 100,
+ "disabled": false
+ }
+ },
+ "ManagementWebhookAdditionalHeaders": {
+ "title": "ManagementWebhookAdditionalHeaders",
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "example": {
+ "X-Custom-Header": "value"
+ }
+ },
+ "UpsertManagementWebhookHeadersRequestBody": {
+ "title": "UpsertManagementWebhookHeadersRequestBody",
+ "type": "object",
+ "properties": {
+ "headers": {
+ "$ref": "#/components/schemas/ManagementWebhookAdditionalHeaders"
+ }
+ },
+ "required": ["headers"],
+ "additionalProperties": false,
+ "example": {
+ "headers": {
+ "X-Custom-Header": "value"
+ }
}
},
- "ManagementWebhookHeadersPatch": {
- "title": "ManagementWebhookHeadersPatch",
+ "GetManagementWebhookHeadersResponse": {
+ "title": "GetManagementWebhookHeadersResponse",
"type": "object",
"properties": {
"headers": {
- "type": "object",
- "additionalProperties": {
+ "$ref": "#/components/schemas/ManagementWebhookAdditionalHeaders"
+ }
+ },
+ "required": ["headers"],
+ "additionalProperties": false,
+ "example": {
+ "headers": {
+ "X-Custom-Header": "value"
+ }
+ }
+ },
+ "DeleteManagementWebhookHeadersRequestBody": {
+ "title": "DeleteManagementWebhookHeadersRequestBody",
+ "type": "object",
+ "properties": {
+ "headers": {
+ "type": "array",
+ "items": {
"type": "string"
- }
+ },
+ "minItems": 1
+ }
+ },
+ "required": ["headers"],
+ "additionalProperties": false,
+ "example": {
+ "headers": ["X-Custom-Header", "X-Another-Header"]
+ }
+ },
+ "DeleteManagementWebhookHeadersResponse": {
+ "title": "DeleteManagementWebhookHeadersResponse",
+ "type": "object",
+ "properties": {
+ "headers": {
+ "$ref": "#/components/schemas/ManagementWebhookAdditionalHeaders"
}
},
- "required": ["headers"]
+ "required": ["headers"],
+ "additionalProperties": false,
+ "example": {
+ "headers": {
+ "X-Remaining-Header": "value"
+ }
+ }
},
"ManagementWebhookHeadersDelete": {
"title": "ManagementWebhookHeadersDelete",
@@ -8599,10 +10176,13 @@
}
}
},
- "required": ["headers"]
+ "required": ["headers"],
+ "example": {
+ "headers": ["X-Custom-Header"]
+ }
},
- "ManagementWebhookHeadersResponse": {
- "title": "ManagementWebhookHeadersResponse",
+ "UpsertManagementWebhookHeadersResponse": {
+ "title": "UpsertManagementWebhookHeadersResponse",
"type": "object",
"properties": {
"headers": {
@@ -8612,36 +10192,58 @@
}
}
},
- "required": ["headers"]
+ "required": ["headers"],
+ "additionalProperties": false,
+ "example": {
+ "headers": {
+ "X-Custom-Header": "value"
+ }
+ }
},
- "ManagementWebhooksResponse": {
- "title": "ManagementWebhooksResponse",
+ "GetManagementWebhooksResponse": {
+ "title": "GetManagementWebhooksResponse",
"type": "object",
"properties": {
- "webhooks": {
+ "data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ManagementWebhook"
}
},
"nextCursor": {
- "type": "string",
- "nullable": true
- }
- }
- },
- "ManagementWebhookResponse": {
- "title": "ManagementWebhookResponse",
- "type": "object",
- "properties": {
- "webhook": {
- "$ref": "#/components/schemas/ManagementWebhook"
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
},
- "required": ["webhook"]
+ "required": ["webhooks", "nextCursor"],
+ "additionalProperties": false,
+ "example": {
+ "data": [
+ {
+ "id": "wh_abc123",
+ "createdAt": "2024-09-03T12:34:56.000Z",
+ "updatedAt": "2024-09-03T12:34:56.000Z",
+ "url": "https://example.com/webhooks",
+ "disabled": false,
+ "subscribedEvents": ["storageUpdated", "userEntered"],
+ "secret": {
+ "value": "whsec_abc123"
+ },
+ "storageUpdatedThrottleSeconds": 10,
+ "yDocUpdatedThrottleSeconds": 10
+ }
+ ],
+ "nextCursor": null
+ }
},
- "ManagementWebhookSecretRotateResponse": {
- "title": "ManagementWebhookSecretRotateResponse",
+ "RotateManagementWebhookSecretResponse": {
+ "title": "RotateManagementWebhookSecretResponse",
"type": "object",
"properties": {
"secret": {
@@ -8651,10 +10253,16 @@
"type": "string"
}
},
- "required": ["secret", "message"]
+ "required": ["secret", "message"],
+ "example": {
+ "secret": {
+ "value": "whsec_new_abc123"
+ },
+ "message": "Previous secret remains valid for 24 hours."
+ }
},
- "ManagementWebhookRecoverRequest": {
- "title": "ManagementWebhookRecoverRequest",
+ "RecoverManagementWebhookFailedMessagesRequestBody": {
+ "title": "RecoverManagementWebhookFailedMessagesRequestBody",
"type": "object",
"properties": {
"since": {
@@ -8662,20 +10270,27 @@
"format": "date-time"
}
},
- "required": ["since"]
+ "required": ["since"],
+ "additionalProperties": false,
+ "example": {
+ "since": "2026-01-21T00:00:00.000Z"
+ }
},
- "ManagementWebhookTestRequest": {
- "title": "ManagementWebhookTestRequest",
+ "TestManagementWebhookRequestBody": {
+ "title": "TestManagementWebhookRequestBody",
"type": "object",
"properties": {
"subscribedEvent": {
"$ref": "#/components/schemas/ManagementWebhookEvent"
}
},
- "required": ["subscribedEvent"]
+ "required": ["subscribedEvent"],
+ "example": {
+ "subscribedEvent": "storageUpdated"
+ }
},
- "ManagementWebhookTestResponse": {
- "title": "ManagementWebhookTestResponse",
+ "TestManagementWebhookResponse": {
+ "title": "TestManagementWebhookResponse",
"type": "object",
"properties": {
"message": {
@@ -8685,56 +10300,29 @@
"type": "string"
},
"deliveredAt": {
- "type": "string",
- "format": "date-time",
- "nullable": true
+ "oneOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
},
- "required": ["id"]
+ "required": ["id"],
+ "additionalProperties": false
}
},
- "required": ["message"]
- },
- "SetPresence": {
- "title": "SetPresence",
- "type": "object",
- "properties": {
- "userId": {
- "type": "string",
- "description": "ID of the user to set presence for"
- },
- "data": {
- "type": "object",
- "description": "Presence data as a JSON object"
- },
- "userInfo": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "minLength": 1,
- "description": "Optional name for the user or agent"
- },
- "avatar": {
- "type": "string",
- "description": "Optional avatar URL for the user"
- },
- "color": {
- "type": "string",
- "description": "Optional color for the user"
- }
- },
- "required": [],
- "description": "Metadata about the user or agent"
- },
- "ttl": {
- "type": "integer",
- "minimum": 2,
- "maximum": 3599,
- "description": "Time-to-live in seconds (minimum: 2, maximum: 3599). After this duration, the presence will automatically expire."
+ "required": ["message"],
+ "additionalProperties": false,
+ "example": {
+ "message": {
+ "id": "msg_abc123",
+ "deliveredAt": "2024-09-03T12:34:56.000Z"
}
- },
- "required": ["userId", "data"]
+ }
}
},
"securitySchemes": {
@@ -8914,7 +10502,7 @@
"error": "ROOM_ALREADY_EXISTS",
"message": "The room already exists.",
"suggestion": "Please use a different room ID or update the existing room.",
- "doc": ""
+ "docs": ""
}
}
}
@@ -9095,7 +10683,7 @@
},
"tags": [
{
- "name": "Authentication"
+ "name": "Auth"
},
{
"name": "Room"
@@ -9123,9 +10711,6 @@
},
{
"name": "Management"
- },
- {
- "name": "Deprecated"
}
],
"x-internal": true
diff --git a/examples/nextjs-ai-dashboard-reports/package-lock.json b/examples/nextjs-ai-dashboard-reports/package-lock.json
index 342c258937..9cae19e82d 100644
--- a/examples/nextjs-ai-dashboard-reports/package-lock.json
+++ b/examples/nextjs-ai-dashboard-reports/package-lock.json
@@ -3761,6 +3761,66 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.7.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.1.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.7.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.1.0",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.0",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+ "version": "2.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
@@ -6324,9 +6384,9 @@
}
},
"node_modules/flatted": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"license": "ISC"
},
"node_modules/for-each": {
diff --git a/examples/nextjs-yjs-quill/package-lock.json b/examples/nextjs-yjs-quill/package-lock.json
index b34827bd04..61ec4c0c2f 100644
--- a/examples/nextjs-yjs-quill/package-lock.json
+++ b/examples/nextjs-yjs-quill/package-lock.json
@@ -575,15 +575,15 @@
}
},
"node_modules/@next/env": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
- "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz",
+ "integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
- "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz",
+ "integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==",
"cpu": [
"arm64"
],
@@ -597,9 +597,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
- "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz",
+ "integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==",
"cpu": [
"x64"
],
@@ -613,9 +613,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
- "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz",
+ "integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==",
"cpu": [
"arm64"
],
@@ -629,9 +629,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
- "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz",
+ "integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==",
"cpu": [
"arm64"
],
@@ -645,9 +645,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
- "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz",
+ "integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==",
"cpu": [
"x64"
],
@@ -661,9 +661,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
- "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz",
+ "integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==",
"cpu": [
"x64"
],
@@ -677,9 +677,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
- "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz",
+ "integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==",
"cpu": [
"arm64"
],
@@ -693,9 +693,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
- "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz",
+ "integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==",
"cpu": [
"x64"
],
@@ -788,12 +788,15 @@
}
},
"node_modules/baseline-browser-mapping": {
- "version": "2.9.18",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
- "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
+ "version": "2.10.8",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
+ "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==",
"license": "Apache-2.0",
"bin": {
- "baseline-browser-mapping": "dist/cli.js"
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
}
},
"node_modules/call-bind": {
@@ -1289,14 +1292,14 @@
}
},
"node_modules/next": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
- "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz",
+ "integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.1.6",
+ "@next/env": "16.1.7",
"@swc/helpers": "0.5.15",
- "baseline-browser-mapping": "^2.8.3",
+ "baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -1308,14 +1311,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.1.6",
- "@next/swc-darwin-x64": "16.1.6",
- "@next/swc-linux-arm64-gnu": "16.1.6",
- "@next/swc-linux-arm64-musl": "16.1.6",
- "@next/swc-linux-x64-gnu": "16.1.6",
- "@next/swc-linux-x64-musl": "16.1.6",
- "@next/swc-win32-arm64-msvc": "16.1.6",
- "@next/swc-win32-x64-msvc": "16.1.6",
+ "@next/swc-darwin-arm64": "16.1.7",
+ "@next/swc-darwin-x64": "16.1.7",
+ "@next/swc-linux-arm64-gnu": "16.1.7",
+ "@next/swc-linux-arm64-musl": "16.1.7",
+ "@next/swc-linux-x64-gnu": "16.1.7",
+ "@next/swc-linux-x64-musl": "16.1.7",
+ "@next/swc-win32-arm64-msvc": "16.1.7",
+ "@next/swc-win32-x64-msvc": "16.1.7",
"sharp": "^0.34.4"
},
"peerDependencies": {
diff --git a/examples/nextjs-yjs-slate/package-lock.json b/examples/nextjs-yjs-slate/package-lock.json
index 8ba36518e9..1501d59371 100644
--- a/examples/nextjs-yjs-slate/package-lock.json
+++ b/examples/nextjs-yjs-slate/package-lock.json
@@ -604,15 +604,15 @@
}
},
"node_modules/@next/env": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
- "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz",
+ "integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
- "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz",
+ "integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==",
"cpu": [
"arm64"
],
@@ -626,9 +626,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
- "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz",
+ "integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==",
"cpu": [
"x64"
],
@@ -642,9 +642,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
- "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz",
+ "integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==",
"cpu": [
"arm64"
],
@@ -658,9 +658,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
- "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz",
+ "integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==",
"cpu": [
"arm64"
],
@@ -674,9 +674,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
- "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz",
+ "integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==",
"cpu": [
"x64"
],
@@ -690,9 +690,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
- "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz",
+ "integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==",
"cpu": [
"x64"
],
@@ -706,9 +706,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
- "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz",
+ "integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==",
"cpu": [
"arm64"
],
@@ -722,9 +722,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
- "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz",
+ "integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==",
"cpu": [
"x64"
],
@@ -848,12 +848,15 @@
}
},
"node_modules/baseline-browser-mapping": {
- "version": "2.9.18",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
- "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
+ "version": "2.10.8",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
+ "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==",
"license": "Apache-2.0",
"bin": {
- "baseline-browser-mapping": "dist/cli.js"
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
}
},
"node_modules/caniuse-lite": {
@@ -1028,14 +1031,14 @@
}
},
"node_modules/next": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
- "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz",
+ "integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.1.6",
+ "@next/env": "16.1.7",
"@swc/helpers": "0.5.15",
- "baseline-browser-mapping": "^2.8.3",
+ "baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -1047,14 +1050,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.1.6",
- "@next/swc-darwin-x64": "16.1.6",
- "@next/swc-linux-arm64-gnu": "16.1.6",
- "@next/swc-linux-arm64-musl": "16.1.6",
- "@next/swc-linux-x64-gnu": "16.1.6",
- "@next/swc-linux-x64-musl": "16.1.6",
- "@next/swc-win32-arm64-msvc": "16.1.6",
- "@next/swc-win32-x64-msvc": "16.1.6",
+ "@next/swc-darwin-arm64": "16.1.7",
+ "@next/swc-darwin-x64": "16.1.7",
+ "@next/swc-linux-arm64-gnu": "16.1.7",
+ "@next/swc-linux-arm64-musl": "16.1.7",
+ "@next/swc-linux-x64-gnu": "16.1.7",
+ "@next/swc-linux-x64-musl": "16.1.7",
+ "@next/swc-win32-arm64-msvc": "16.1.7",
+ "@next/swc-win32-x64-msvc": "16.1.7",
"sharp": "^0.34.4"
},
"peerDependencies": {
diff --git a/examples/nextjs-yjs-tiptap/package-lock.json b/examples/nextjs-yjs-tiptap/package-lock.json
index 889729d8c9..ffe6bb9f32 100644
--- a/examples/nextjs-yjs-tiptap/package-lock.json
+++ b/examples/nextjs-yjs-tiptap/package-lock.json
@@ -603,15 +603,15 @@
}
},
"node_modules/@next/env": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
- "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz",
+ "integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
- "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz",
+ "integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==",
"cpu": [
"arm64"
],
@@ -625,9 +625,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
- "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz",
+ "integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==",
"cpu": [
"x64"
],
@@ -641,9 +641,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
- "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz",
+ "integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==",
"cpu": [
"arm64"
],
@@ -657,9 +657,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
- "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz",
+ "integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==",
"cpu": [
"arm64"
],
@@ -673,9 +673,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
- "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz",
+ "integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==",
"cpu": [
"x64"
],
@@ -689,9 +689,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
- "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz",
+ "integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==",
"cpu": [
"x64"
],
@@ -705,9 +705,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
- "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz",
+ "integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==",
"cpu": [
"arm64"
],
@@ -721,9 +721,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
- "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz",
+ "integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==",
"cpu": [
"x64"
],
@@ -1331,12 +1331,15 @@
"license": "Python-2.0"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.9.18",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
- "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
+ "version": "2.10.8",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
+ "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==",
"license": "Apache-2.0",
"bin": {
- "baseline-browser-mapping": "dist/cli.js"
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
}
},
"node_modules/caniuse-lite": {
@@ -1538,14 +1541,14 @@
}
},
"node_modules/next": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
- "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz",
+ "integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.1.6",
+ "@next/env": "16.1.7",
"@swc/helpers": "0.5.15",
- "baseline-browser-mapping": "^2.8.3",
+ "baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -1557,14 +1560,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.1.6",
- "@next/swc-darwin-x64": "16.1.6",
- "@next/swc-linux-arm64-gnu": "16.1.6",
- "@next/swc-linux-arm64-musl": "16.1.6",
- "@next/swc-linux-x64-gnu": "16.1.6",
- "@next/swc-linux-x64-musl": "16.1.6",
- "@next/swc-win32-arm64-msvc": "16.1.6",
- "@next/swc-win32-x64-msvc": "16.1.6",
+ "@next/swc-darwin-arm64": "16.1.7",
+ "@next/swc-darwin-x64": "16.1.7",
+ "@next/swc-linux-arm64-gnu": "16.1.7",
+ "@next/swc-linux-arm64-musl": "16.1.7",
+ "@next/swc-linux-x64-gnu": "16.1.7",
+ "@next/swc-linux-x64-musl": "16.1.7",
+ "@next/swc-win32-arm64-msvc": "16.1.7",
+ "@next/swc-win32-x64-msvc": "16.1.7",
"sharp": "^0.34.4"
},
"peerDependencies": {
diff --git a/examples/redux-whiteboard/package-lock.json b/examples/redux-whiteboard/package-lock.json
index 7584b46ed8..3e0df6955a 100644
--- a/examples/redux-whiteboard/package-lock.json
+++ b/examples/redux-whiteboard/package-lock.json
@@ -533,15 +533,15 @@
}
},
"node_modules/@next/env": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
- "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz",
+ "integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
- "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz",
+ "integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==",
"cpu": [
"arm64"
],
@@ -555,9 +555,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
- "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz",
+ "integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==",
"cpu": [
"x64"
],
@@ -571,9 +571,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
- "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz",
+ "integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==",
"cpu": [
"arm64"
],
@@ -587,9 +587,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
- "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz",
+ "integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==",
"cpu": [
"arm64"
],
@@ -603,9 +603,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
- "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz",
+ "integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==",
"cpu": [
"x64"
],
@@ -619,9 +619,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
- "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz",
+ "integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==",
"cpu": [
"x64"
],
@@ -635,9 +635,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
- "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz",
+ "integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==",
"cpu": [
"arm64"
],
@@ -651,9 +651,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
- "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz",
+ "integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==",
"cpu": [
"x64"
],
@@ -790,12 +790,15 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.9.18",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
- "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
+ "version": "2.10.8",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
+ "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==",
"license": "Apache-2.0",
"bin": {
- "baseline-browser-mapping": "dist/cli.js"
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
}
},
"node_modules/caniuse-lite": {
@@ -888,14 +891,14 @@
}
},
"node_modules/next": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
- "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
+ "version": "16.1.7",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz",
+ "integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.1.6",
+ "@next/env": "16.1.7",
"@swc/helpers": "0.5.15",
- "baseline-browser-mapping": "^2.8.3",
+ "baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -907,14 +910,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.1.6",
- "@next/swc-darwin-x64": "16.1.6",
- "@next/swc-linux-arm64-gnu": "16.1.6",
- "@next/swc-linux-arm64-musl": "16.1.6",
- "@next/swc-linux-x64-gnu": "16.1.6",
- "@next/swc-linux-x64-musl": "16.1.6",
- "@next/swc-win32-arm64-msvc": "16.1.6",
- "@next/swc-win32-x64-msvc": "16.1.6",
+ "@next/swc-darwin-arm64": "16.1.7",
+ "@next/swc-darwin-x64": "16.1.7",
+ "@next/swc-linux-arm64-gnu": "16.1.7",
+ "@next/swc-linux-arm64-musl": "16.1.7",
+ "@next/swc-linux-x64-gnu": "16.1.7",
+ "@next/swc-linux-x64-musl": "16.1.7",
+ "@next/swc-win32-arm64-msvc": "16.1.7",
+ "@next/swc-win32-x64-msvc": "16.1.7",
"sharp": "^0.34.4"
},
"peerDependencies": {
diff --git a/package-lock.json b/package-lock.json
index 90b938dc4c..8848e13f19 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,8 @@
"shared/*",
"packages/*",
"!packages/liveblocks-server",
+ "!packages/liveblocks-python",
+ "!packages/liveblocks-python-codegen",
"tools/*",
"!tools/liveblocks-cli",
"e2e/next-ai-kitchen-sink",
diff --git a/package.json b/package.json
index 800e2b7c83..a4abb12c06 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,8 @@
"shared/*",
"packages/*",
"!packages/liveblocks-server",
+ "!packages/liveblocks-python",
+ "!packages/liveblocks-python-codegen",
"tools/*",
"!tools/liveblocks-cli",
"e2e/next-ai-kitchen-sink",
diff --git a/packages/liveblocks-python-codegen/.gitignore b/packages/liveblocks-python-codegen/.gitignore
new file mode 100644
index 0000000000..dce1d0fe98
--- /dev/null
+++ b/packages/liveblocks-python-codegen/.gitignore
@@ -0,0 +1,16 @@
+__pycache__/
+
+# pyenv
+.python-version
+
+# Environments
+.env
+.venv
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# JetBrains
+.idea/
diff --git a/packages/liveblocks-python-codegen/README.md b/packages/liveblocks-python-codegen/README.md
new file mode 100644
index 0000000000..d79ae55bc1
--- /dev/null
+++ b/packages/liveblocks-python-codegen/README.md
@@ -0,0 +1,80 @@
+# liveblocks-python-codegen
+
+Code generator for the [Liveblocks Python SDK](../liveblocks-python/). Uses
+[openapi-python-client](https://github.com/openapi-generators/openapi-python-client)
+as a library to parse the OpenAPI spec and generate the SDK source code, along
+with two README variants:
+
+- `README.md` — standard Markdown (for PyPI / GitHub)
+- `README.mdx` — rich MDX with components like `` (for the docs
+ site)
+
+Both are rendered from Jinja templates in `templates/` using the same parsed
+OpenAPI context, so they never drift apart.
+
+## Prerequisites
+
+- Python 3.12+
+- [uv](https://docs.astral.sh/uv/) (manages the Python environment and
+ dependencies automatically)
+
+## Usage
+
+From this directory:
+
+```bash
+uv run python generate.py
+```
+
+This will:
+
+1. Parse `../../docs/references/v2.openapi.json`
+2. Generate the full SDK (models, client, endpoints, `README.md`) into
+ `../liveblocks-python/`
+3. Run `ruff` to lint and format the generated Python code (configured as
+ post-hooks in `config.yaml`)
+4. Render `README.mdx` into `../liveblocks-python/`
+
+The first run installs dependencies (pinned in `pyproject.toml`) into a local
+`.venv`. Subsequent runs reuse the cached environment.
+
+## Project structure
+
+```
+liveblocks-python-codegen/
+├── pyproject.toml # Pins openapi-python-client version
+├── config.yaml # Generator config (package name, post-hooks)
+├── generate.py # Single entry point
+└── templates/ # Jinja templates for all generated files
+ ├── _shared.jinja # Shared macros used across templates
+ ├── README.md.jinja # PyPI / GitHub README
+ ├── README.mdx.jinja # Docs-site README (rendered separately)
+ ├── client.py.jinja # LiveblocksClient class
+ ├── endpoint_module.py.jinja # One file per API endpoint group
+ ├── endpoint_macros.py.jinja # Shared endpoint helpers
+ ├── model.py.jinja # Pydantic model per schema
+ ├── types.py.jinja # Shared type definitions
+ ├── errors.py.jinja # Exception classes
+ ├── pyproject_uv.toml.jinja # Generated SDK pyproject.toml
+ └── ... # Init files, enums, ruff config, etc.
+```
+
+## Editing templates
+
+All output files are driven by Jinja templates in `templates/`. To change the
+generated SDK:
+
+1. Edit the relevant `.jinja` template.
+2. Re-run `uv run python generate.py` to regenerate.
+3. Review the diff in `../liveblocks-python/`.
+
+Templates receive the parsed OpenAPI data as context — see the
+[openapi-python-client custom templates docs](https://github.com/openapi-generators/openapi-python-client?tab=readme-ov-file#custom-templates)
+for the available variables.
+
+## Updating the generator version
+
+```bash
+uv add openapi-python-client==
+uv run python generate.py
+```
diff --git a/packages/liveblocks-python-codegen/config.yaml b/packages/liveblocks-python-codegen/config.yaml
new file mode 100644
index 0000000000..b01d421371
--- /dev/null
+++ b/packages/liveblocks-python-codegen/config.yaml
@@ -0,0 +1,8 @@
+project_name_override: liveblocks
+package_name_override: liveblocks
+
+package_version_override: 0.1.0
+
+post_hooks:
+ - "uvx ruff check --fix-only ."
+ - "uvx ruff format ."
diff --git a/packages/liveblocks-python-codegen/generate.py b/packages/liveblocks-python-codegen/generate.py
new file mode 100644
index 0000000000..c1b761c1cd
--- /dev/null
+++ b/packages/liveblocks-python-codegen/generate.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+"""Generate the Liveblocks Python SDK, README.md, and README.mdx."""
+
+import json
+import sys
+from pathlib import Path
+
+from openapi_python_client import Project
+from openapi_python_client.config import Config, ConfigFile, MetaType
+from openapi_python_client.parser import GeneratorData
+from openapi_python_client.parser.errors import GeneratorError
+
+CODEGEN_DIR = Path(__file__).parent.resolve()
+SDK_DIR = (CODEGEN_DIR / "../liveblocks-python").resolve()
+SPEC = (CODEGEN_DIR / "../../docs/references/v2.openapi.json").resolve()
+
+
+def main() -> None:
+ if not SPEC.exists():
+ print(f"OpenAPI spec not found: {SPEC}", file=sys.stderr)
+ sys.exit(1)
+
+ config_file = ConfigFile.load_from_path(CODEGEN_DIR / "config.yaml")
+ config = Config.from_sources(
+ config_file=config_file,
+ meta_type=MetaType.UV,
+ document_source=SPEC,
+ file_encoding="utf-8",
+ overwrite=True,
+ output_path=SDK_DIR,
+ )
+
+ data_dict = json.loads(SPEC.read_bytes())
+ openapi = GeneratorData.from_dict(data_dict, config=config)
+ if isinstance(openapi, GeneratorError):
+ print(f"Failed to parse OpenAPI: {openapi.header} — {openapi.detail}", file=sys.stderr)
+ sys.exit(1)
+
+ project = Project(
+ openapi=openapi,
+ config=config,
+ custom_template_path=CODEGEN_DIR / "templates",
+ )
+
+ errors = project.build()
+ for error in errors:
+ print(f" {error.level.name}: {error.header}: {error.detail}")
+ print(f"Generated {SDK_DIR}")
+
+ mdx_template = project.env.get_template("README.mdx.jinja")
+ mdx_path = SDK_DIR / "README.mdx"
+ mdx_path.write_text(
+ mdx_template.render(meta=config.meta_type),
+ encoding="utf-8",
+ )
+ print(f"Generated {mdx_path}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packages/liveblocks-python-codegen/pyproject.toml b/packages/liveblocks-python-codegen/pyproject.toml
new file mode 100644
index 0000000000..4c76f69eea
--- /dev/null
+++ b/packages/liveblocks-python-codegen/pyproject.toml
@@ -0,0 +1,8 @@
+[project]
+name = "liveblocks-python-codegen"
+version = "0.1.0"
+description = "Code generator for the Liveblocks Python SDK"
+requires-python = ">=3.12"
+dependencies = [
+ "openapi-python-client==0.28.2",
+]
diff --git a/packages/liveblocks-python-codegen/templates/.gitignore.jinja b/packages/liveblocks-python-codegen/templates/.gitignore.jinja
new file mode 100644
index 0000000000..79a2c3d73c
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/.gitignore.jinja
@@ -0,0 +1,23 @@
+__pycache__/
+build/
+dist/
+*.egg-info/
+.pytest_cache/
+
+# pyenv
+.python-version
+
+# Environments
+.env
+.venv
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# JetBrains
+.idea/
+
+/coverage.xml
+/.coverage
diff --git a/packages/liveblocks-python-codegen/templates/README.md.jinja b/packages/liveblocks-python-codegen/templates/README.md.jinja
new file mode 100644
index 0000000000..79c358c85c
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/README.md.jinja
@@ -0,0 +1,185 @@
+{% from "endpoint_macros.py.jinja" import return_type, arguments %}
+{#
+ Render a property as a valid Python literal for use in examples.
+
+ | Type | With example | Without example |
+ |------------|-------------------------|-----------------|
+ | str | "" | "..." |
+ | int | | 0 |
+ | float | | 0.0 |
+ | bool | | True |
+ | list[...] | [] | [] |
+ | dict[...] | {} | {} |
+ | other | ... | ... |
+#}
+{% macro example_value(prop) -%}
+{%- set _base = prop.get_base_type_string() -%}
+{%- set _types = _base.split(" | ") -%}
+{%- if "str" in _types -%}
+{%- if prop.example is string -%}"{{ prop.example | replace('\\', '\\\\') | replace('"', '\\"') }}"{%- else -%}"..."{%- endif -%}
+{%- elif "int" in _types -%}
+{{ prop.example if prop.example is number else 0 }}
+{%- elif "float" in _types -%}
+{{ prop.example if prop.example is number else 0.0 }}
+{%- elif "bool" in _types -%}
+{{ prop.example if prop.example in [true, false] else "True" }}
+{%- elif _base.startswith("list[") -%}
+[]
+{%- elif _base.startswith("dict[") -%}
+{}
+{%- else -%}
+...
+{%- endif -%}
+{%- endmacro %}
+# @liveblocks/python
+
+`@liveblocks/python` provides you with a Python client for accessing the Liveblocks API. This library is only intended for use in your Python back end.
+
+## Installation
+
+Install the Liveblocks package to get started.
+
+```bash
+pip install {{ project_name }}
+```
+
+## Quickstart
+
+All API calls require a Liveblocks client set up with your secret key. Find your key in the [Liveblocks Dashboard](https://liveblocks.io/dashboard/apikeys).
+
+### Synchronous
+
+```python
+from {{ package_name }} import Liveblocks
+
+client = Liveblocks(secret="{% raw %}{{SECRET_KEY}}{% endraw %}")
+
+with client:
+ rooms = client.get_rooms()
+ print(rooms)
+```
+
+### Asynchronous
+
+```python
+from {{ package_name }} import AsyncLiveblocks
+
+client = AsyncLiveblocks(secret="{% raw %}{{SECRET_KEY}}{% endraw %}")
+
+async with client:
+ rooms = await client.get_rooms()
+ print(rooms)
+```
+
+---
+
+## API Reference
+
+{% for tag, collection in endpoint_collections_by_tag.items() %}
+### {{ collection.tag | capitalize }}
+
+{% for endpoint in collection.endpoints %}
+{% if endpoint.name not in hidden_endpoints %}
+{% set ns = namespace(success_responses=[]) %}
+{% for response in endpoint.responses.patterns %}
+{% set code_range = response.status_code.range %}
+{% if code_range[0] >= 200 and code_range[1] <= 299 %}
+{% set ns.success_responses = ns.success_responses + [response] %}
+{% endif %}
+{% endfor %}
+{% set method_name = endpoint.name | snakecase %}
+{% set ret_type = return_type(ns.success_responses) %}
+#### `{{ method_name }}`
+
+{% if endpoint.description %}
+{{ endpoint.description | replace('\\"', '"') }}
+{% endif %}
+
+{% set body_prop = (endpoint.bodies | selectattr("prop.required") | first).prop if endpoint.bodies | selectattr("prop.required") | list else None %}
+{% if body_prop %}
+{% set body_type = body_prop.get_type_string() %}
+{% set body_is_simple = "|" in body_type or "list[" in body_type or body_type in ["File", "Any"] %}
+{% set body_required = (body_prop.required_properties | selectattr("default", "none") | list) if body_prop.required_properties else [] %}
+{% set body_optional = (body_prop.optional_properties or [])[:3] %}
+{% endif %}
+**Example**
+```python
+{% if body_prop and not body_is_simple %}
+from {{ package_name }}.models import {{ body_type }}
+
+{% endif %}
+{% if endpoint.path_parameters or endpoint.query_parameters or body_prop %}
+{% if ret_type != "None" %}result = {% endif %}client.{{ method_name }}(
+{% for p in endpoint.path_parameters %}
+ {{ p.python_name }}={{ example_value(p) }},
+{% endfor %}
+{% for p in endpoint.query_parameters | selectattr("required") %}
+ {{ p.python_name }}={{ example_value(p) }},
+{% endfor %}
+{% if body_prop %}
+{% if body_is_simple %}
+ body=...,
+{% elif body_required or body_optional %}
+ body={{ body_type }}(
+{% for f in body_required %}
+ {{ f.python_name }}={{ example_value(f) }},
+{% endfor %}
+{% for f in body_optional %}
+ # {{ f.python_name }}={{ example_value(f) }},
+{% endfor %}
+ ),
+{% else %}
+ body={{ body_type }}(...),
+{% endif %}
+{% endif %}
+{% for p in endpoint.query_parameters | rejectattr("required") %}
+ # {{ p.python_name }}={{ example_value(p) }},
+{% endfor %}
+)
+{% else %}
+{% if ret_type != "None" %}result = {% endif %}client.{{ method_name }}()
+{% endif %}
+{% if ret_type != "None" %}
+print(result)
+{% endif %}
+```
+{% if endpoint.path_parameters or endpoint.query_parameters or endpoint.bodies %}
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+{% for param in endpoint.path_parameters %}
+| `{{ param.python_name }}` | `{{ param.get_type_string() | replace("|", "\\|") }}` | Yes | {{ (param.description or "") | replace('\\"', '"') }} |
+{% endfor %}
+{% for param in endpoint.query_parameters %}
+| `{{ param.python_name }}` | `{{ param.get_type_string() | replace("|", "\\|") }}` | {{ "Yes" if param.required else "No" }} | {{ (param.description or "") | replace('\\"', '"') }}{% if param.default %} *(default: `{{ param.default.python_code }}`)*{% endif %} |
+{% endfor %}
+{% for body in endpoint.bodies %}
+| `body` | `{{ body.prop.get_type_string() | replace("|", "\\|") }}` | {{ "Yes" if body.prop.required else "No" }} | Request body ({{ body.content_type }}) |
+{% endfor %}
+
+{% endif %}
+
+---
+
+{% endif %}
+{% endfor %}
+{% endfor %}
+
+## Error Handling
+
+All API methods raise `errors.LiveblocksError` when the server returns a non-2xx status code. You can catch and inspect these errors:
+
+```python
+from {{ package_name }} import errors, Liveblocks
+
+client = Liveblocks(secret="sk_your_secret_key")
+
+with client:
+ try:
+ room = client.get_room(room_id="my-room")
+ except errors.LiveblocksError as e:
+ print(f"API error: {e}")
+```
+
+Methods also raise `httpx.TimeoutException` if the request exceeds the timeout.
\ No newline at end of file
diff --git a/packages/liveblocks-python-codegen/templates/README.mdx.jinja b/packages/liveblocks-python-codegen/templates/README.mdx.jinja
new file mode 100644
index 0000000000..b2f6bec2d8
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/README.mdx.jinja
@@ -0,0 +1,198 @@
+{% from "endpoint_macros.py.jinja" import return_type, arguments %}
+{#
+ Render a property as a valid Python literal for use in examples.
+
+ | Type | With example | Without example |
+ |------------|-------------------------|-----------------|
+ | str | "" | "..." |
+ | int | | 0 |
+ | float | | 0.0 |
+ | bool | | True |
+ | list[...] | [] | [] |
+ | dict[...] | {} | {} |
+ | other | ... | ... |
+#}
+{% macro example_value(prop) -%}
+{%- set _base = prop.get_base_type_string() -%}
+{%- set _types = _base.split(" | ") -%}
+{%- if "str" in _types -%}
+{%- if prop.example is string -%}"{{ prop.example | replace('\\', '\\\\') | replace('"', '\\"') }}"{%- else -%}"..."{%- endif -%}
+{%- elif "int" in _types -%}
+{{ prop.example if prop.example is number else 0 }}
+{%- elif "float" in _types -%}
+{{ prop.example if prop.example is number else 0.0 }}
+{%- elif "bool" in _types -%}
+{{ prop.example if prop.example in [true, false] else "True" }}
+{%- elif _base.startswith("list[") -%}
+[]
+{%- elif _base.startswith("dict[") -%}
+{}
+{%- else -%}
+...
+{%- endif -%}
+{%- endmacro %}
+---
+meta:
+ title: "@liveblocks/python"
+ parentTitle: "API Reference"
+ description: "API Reference for the @liveblocks/python package"
+alwaysShowAllNavigationLevels: false
+---
+
+`@liveblocks/python` provides you with a Python client for accessing the Liveblocks API. This library is only intended for use in your Python back end.
+
+## Installation
+
+Install the Liveblocks package to get started.
+
+```bash
+pip install {{ project_name }}
+```
+
+## Quickstart
+
+All API calls require a Liveblocks client configured with your secret key, found in the [Liveblocks Dashboard](https://liveblocks.io/dashboard/apikeys). Methods can be called synchronously or asynchronously.
+
+```python title="Synchronous"
+from {{ package_name }} import Liveblocks
+
+client = Liveblocks(secret="{% raw %}{{SECRET_KEY}}{% endraw %}")
+
+with client:
+ rooms = client.get_rooms()
+ print(rooms)
+```
+
+```python title="Asynchronous"
+from {{ package_name }} import AsyncLiveblocks
+
+client = AsyncLiveblocks(secret="{% raw %}{{SECRET_KEY}}{% endraw %}")
+
+async with client:
+ rooms = await client.get_rooms()
+ print(rooms)
+```
+
+{% for tag, collection in endpoint_collections_by_tag.items() %}
+## {{ collection.tag | capitalize }}
+
+{% for endpoint in collection.endpoints %}
+{% if endpoint.name not in hidden_endpoints %}
+{% set ns = namespace(success_responses=[]) %}
+{% for response in endpoint.responses.patterns %}
+{% set code_range = response.status_code.range %}
+{% if code_range[0] >= 200 and code_range[1] <= 299 %}
+{% set ns.success_responses = ns.success_responses + [response] %}
+{% endif %}
+{% endfor %}
+{% set ret_type = return_type(ns.success_responses) %}
+{% set method_name = endpoint.name | snakecase %}
+{% set all_params = endpoint.list_all_parameters() %}
+### {{ method_name }}
+
+{% if endpoint.description %}
+{{ endpoint.description | replace('\\"', '"') }}
+{% endif %}
+
+{% set body_prop = (endpoint.bodies | selectattr("prop.required") | first).prop if endpoint.bodies | selectattr("prop.required") | list else None %}
+{% if body_prop %}
+{% set body_type = body_prop.get_type_string() %}
+{% set body_is_simple = "|" in body_type or "list[" in body_type or body_type in ["File", "Any"] %}
+{% set body_required = (body_prop.required_properties | selectattr("default", "none") | list) if body_prop.required_properties else [] %}
+{% set body_optional = (body_prop.optional_properties or [])[:3] %}
+{% endif %}
+```python
+{% if body_prop and not body_is_simple %}
+from {{ package_name }}.models import {{ body_type }}
+
+{% endif %}
+{% if endpoint.path_parameters or endpoint.query_parameters or body_prop %}
+{% if ret_type != "None" %}result = {% endif %}client.{{ method_name }}(
+{% for p in endpoint.path_parameters %}
+ {{ p.python_name }}={{ example_value(p) }},
+{% endfor %}
+{% for p in endpoint.query_parameters | selectattr("required") %}
+ {{ p.python_name }}={{ example_value(p) }},
+{% endfor %}
+{% if body_prop %}
+{% if body_is_simple %}
+ body=...,
+{% elif body_required or body_optional %}
+ body={{ body_type }}(
+{% for f in body_required %}
+ {{ f.python_name }}={{ example_value(f) }},
+{% endfor %}
+{% for f in body_optional %}
+ # {{ f.python_name }}={{ example_value(f) }},
+{% endfor %}
+ ),
+{% else %}
+ body={{ body_type }}(...),
+{% endif %}
+{% endif %}
+{% for p in endpoint.query_parameters | rejectattr("required") %}
+ # {{ p.python_name }}={{ example_value(p) }},
+{% endfor %}
+)
+{% else %}
+{% if ret_type != "None" %}result = {% endif %}client.{{ method_name }}()
+{% endif %}
+{% if ret_type != "None" %}
+print(result)
+{% endif %}
+```
+
+{% if endpoint.path_parameters or endpoint.query_parameters or endpoint.bodies %}
+
+{% for param in endpoint.path_parameters %}
+
+ {{ (param.description or "") | replace('\\"', '"') }}
+
+
+{% endfor %}
+{% for param in endpoint.query_parameters %}
+
+ {{ (param.description or "") | replace('\\"', '"') }}{% if param.default %} *(default: `{{ param.default.python_code }}`)*{% endif %}
+
+
+{% endfor %}
+{% for body in endpoint.bodies %}
+
+ Request body ({{ body.content_type }}).
+
+
+{% endfor %}
+
+
+{% endif %}
+
+{% endif %}
+{% endfor %}
+{% endfor %}
+
+## Error Handling
+
+All API methods raise `errors.LiveblocksError` when the server returns a non-2xx status code. You can catch and inspect these errors:
+
+```python
+from {{ package_name }} import errors, Liveblocks
+
+client = Liveblocks(secret="sk_your_secret_key")
+
+with client:
+ try:
+ room = client.get_room(room_id="my-room")
+ except errors.LiveblocksError as e:
+ print(f"API error: {e}")
+```
+
+Methods also raise `httpx.TimeoutException` if the request exceeds the timeout.
\ No newline at end of file
diff --git a/packages/liveblocks-python-codegen/templates/_shared.jinja b/packages/liveblocks-python-codegen/templates/_shared.jinja
new file mode 100644
index 0000000000..0d00ce0d18
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/_shared.jinja
@@ -0,0 +1,16 @@
+{#
+ Endpoints and models listed here by operationId / class name are completely
+ suppressed from the generated SDK (empty files, no exports, no client methods,
+ no README entries). We have to hardcode these because the generator does not
+ expose a way to skip code generation from within templates.
+#}
+{% set hidden_endpoints = [
+ "get-room-notification-settings",
+ "update-room-notification-settings",
+ "delete-room-notification-settings",
+ "get-thread-participants",
+] %}
+
+{% set hidden_models = [
+ "GetThreadParticipantsResponse",
+] %}
diff --git a/packages/liveblocks-python-codegen/templates/api_init.py.jinja b/packages/liveblocks-python-codegen/templates/api_init.py.jinja
new file mode 100644
index 0000000000..dc035f4ce8
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/api_init.py.jinja
@@ -0,0 +1 @@
+""" Contains methods for accessing the API """
diff --git a/packages/liveblocks-python-codegen/templates/client.py.jinja b/packages/liveblocks-python-codegen/templates/client.py.jinja
new file mode 100644
index 0000000000..0c08cb3858
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/client.py.jinja
@@ -0,0 +1,216 @@
+from __future__ import annotations
+
+import re
+import warnings
+from typing import Any, TYPE_CHECKING
+
+import httpx
+
+from .types import File, Response, UNSET, Unset
+
+{% from "endpoint_macros.py.jinja" import arguments, kwargs, return_type, docstring %}
+{% from "_shared.jinja" import hidden_endpoints %}
+
+{#
+ Gather every model import needed across all endpoints, deduplicate them,
+ rewrite the relative path (from ... → from .), filter out hidden models,
+ and emit them under TYPE_CHECKING so they're available for type hints
+ without creating circular imports at runtime.
+#}
+if TYPE_CHECKING:
+{% set ns = namespace(imports=[]) %}
+{% for tag, collection in endpoint_collections_by_tag.items() %}
+{% for endpoint in collection.endpoints %}
+{% if endpoint.name not in hidden_endpoints %}
+{% for _import in endpoint.relative_imports | sort %}
+{% set import = _import | replace("from ...", "from .") %}
+{% if import not in ns.imports and "from .models." in import %}
+ {{ import }}
+{% set ns.imports = ns.imports + [import] %}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% endfor %}
+{% endfor %}
+
+ from .session import AsyncSession, Session
+
+_DEFAULT_BASE_URL = "https://api.liveblocks.io"
+_VALID_KEY_CHARS_REGEX = re.compile(r"^[\w-]+$")
+
+
+def _assert_secret_key(value: str) -> None:
+ if not value.startswith("sk_"):
+ raise ValueError(
+ "Invalid value for 'secret'. Secret keys must start with 'sk_'. "
+ "Please provide the secret key from your Liveblocks dashboard at "
+ "https://liveblocks.io/dashboard/apikeys."
+ )
+ if not _VALID_KEY_CHARS_REGEX.match(value):
+ raise ValueError(
+ "Invalid chars found in 'secret'. Please check that you correctly "
+ "copied the secret key from your Liveblocks dashboard at "
+ "https://liveblocks.io/dashboard/apikeys."
+ )
+
+
+class Liveblocks:
+ """Synchronous client for the Liveblocks API.
+
+ Args:
+ secret: The Liveblocks secret key. Must start with ``sk_``.
+ Get it from https://liveblocks.io/dashboard/apikeys
+ base_url: Point the client to an alternative Liveblocks server.
+ """
+
+ _client: httpx.Client
+
+ def __init__(self, *, secret: str, base_url: str | None = None) -> None:
+ _assert_secret_key(secret)
+ self._client = httpx.Client(
+ base_url=base_url or _DEFAULT_BASE_URL,
+ headers={"Authorization": f"Bearer {secret}"},
+ )
+
+ def close(self) -> None:
+ self._client.close()
+
+ def __enter__(self) -> Liveblocks:
+ self._client.__enter__()
+ return self
+
+ def __exit__(self, *args: Any, **kwargs: Any) -> None:
+ self._client.__exit__(*args, **kwargs)
+
+ def prepare_session(
+ self,
+ user_id: str,
+ user_info: dict[str, Any] | None = None,
+ organization_id: str | None = None,
+ ) -> Session:
+ from .session import Session
+
+ return Session(
+ client=self,
+ user_id=user_id,
+ user_info=user_info,
+ organization_id=organization_id,
+ )
+
+ {#
+ Generate one public method per non-hidden endpoint. Each method:
+ 1. Collects 2xx responses to derive the return type
+ 2. Emits a deprecation warning when the spec marks the operation deprecated
+ 3. Lazily imports the corresponding api module and delegates to its _sync helper
+ #}
+ {% for tag, collection in endpoint_collections_by_tag.items() %}
+ {% for endpoint in collection.endpoints %}
+ {% if endpoint.name not in hidden_endpoints %}
+
+ {# Filter to only 2xx success responses -- these determine the return type. #}
+ {% set ns = namespace(success_responses=[]) %}
+ {% for response in endpoint.responses.patterns %}
+ {% set code_range = response.status_code.range %}
+ {% if code_range[0] >= 200 and code_range[1] <= 299 %}
+ {% set ns.success_responses = ns.success_responses + [response] %}
+ {% endif %}
+ {% endfor %}
+
+ def {{ endpoint.name | snakecase }}(
+ self,
+ {{ arguments(endpoint, include_client=False) | indent(8) }}
+ ) -> ({{ return_type(ns.success_responses) }}):
+ {{ docstring(endpoint, return_type(ns.success_responses)) | indent(8) }}
+{% if endpoint.description and "Deprecated" in endpoint.description %}
+ warnings.warn(
+ "{{ endpoint.name | snakecase }} is deprecated",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+{% endif %}
+ from .api.{{ tag }} import {{ endpoint.name | snakecase }}
+
+ return {{ endpoint.name | snakecase }}._sync(
+ {{ kwargs(endpoint, include_client=False) | indent(12) }}client=self._client,
+ )
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+
+
+class AsyncLiveblocks:
+ """Asynchronous client for the Liveblocks API.
+
+ Args:
+ secret: The Liveblocks secret key. Must start with ``sk_``.
+ Get it from https://liveblocks.io/dashboard/apikeys
+ base_url: Point the client to an alternative Liveblocks server.
+ """
+
+ _client: httpx.AsyncClient
+
+ def __init__(self, *, secret: str, base_url: str | None = None) -> None:
+ _assert_secret_key(secret)
+ self._client = httpx.AsyncClient(
+ base_url=base_url or _DEFAULT_BASE_URL,
+ headers={"Authorization": f"Bearer {secret}"},
+ )
+
+ async def close(self) -> None:
+ await self._client.aclose()
+
+ async def __aenter__(self) -> AsyncLiveblocks:
+ await self._client.__aenter__()
+ return self
+
+ async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
+ await self._client.__aexit__(*args, **kwargs)
+
+ def prepare_session(
+ self,
+ user_id: str,
+ user_info: dict[str, Any] | None = None,
+ organization_id: str | None = None,
+ ) -> AsyncSession:
+ from .session import AsyncSession
+
+ return AsyncSession(
+ client=self,
+ user_id=user_id,
+ user_info=user_info,
+ organization_id=organization_id,
+ )
+
+ {# Same pattern as Liveblocks above, but delegates to _asyncio instead of _sync. #}
+ {% for tag, collection in endpoint_collections_by_tag.items() %}
+ {% for endpoint in collection.endpoints %}
+ {% if endpoint.name not in hidden_endpoints %}
+
+ {% set ns = namespace(success_responses=[]) %}
+ {% for response in endpoint.responses.patterns %}
+ {% set code_range = response.status_code.range %}
+ {% if code_range[0] >= 200 and code_range[1] <= 299 %}
+ {% set ns.success_responses = ns.success_responses + [response] %}
+ {% endif %}
+ {% endfor %}
+
+ async def {{ endpoint.name | snakecase }}(
+ self,
+ {{ arguments(endpoint, include_client=False) | indent(8) }}
+ ) -> ({{ return_type(ns.success_responses) }}):
+ {{ docstring(endpoint, return_type(ns.success_responses)) | indent(8) }}
+{% if endpoint.description and "Deprecated" in endpoint.description %}
+ warnings.warn(
+ "{{ endpoint.name | snakecase }} is deprecated",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+{% endif %}
+ from .api.{{ tag }} import {{ endpoint.name | snakecase }}
+
+ return await {{ endpoint.name | snakecase }}._asyncio(
+ {{ kwargs(endpoint, include_client=False) | indent(12) }}client=self._client,
+ )
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
diff --git a/packages/liveblocks-python-codegen/templates/endpoint_init.py.jinja b/packages/liveblocks-python-codegen/templates/endpoint_init.py.jinja
new file mode 100644
index 0000000000..c9921b5fd9
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/endpoint_init.py.jinja
@@ -0,0 +1 @@
+""" Contains endpoint functions for accessing the API """
diff --git a/packages/liveblocks-python-codegen/templates/endpoint_macros.py.jinja b/packages/liveblocks-python-codegen/templates/endpoint_macros.py.jinja
new file mode 100644
index 0000000000..b9fc263ff9
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/endpoint_macros.py.jinja
@@ -0,0 +1,245 @@
+{% from "property_templates/helpers.jinja" import guarded_statement %}
+{% from "helpers.jinja" import safe_docstring %}
+
+{% macro header_params(endpoint) %}
+{% if endpoint.header_parameters or endpoint.bodies | length > 0 %}
+headers: dict[str, Any] = {}
+{% if endpoint.header_parameters %}
+ {% for parameter in endpoint.header_parameters %}
+ {% import "property_templates/" + parameter.template as param_template %}
+ {% if param_template.transform_header %}
+ {% set expression = param_template.transform_header(parameter.python_name) %}
+ {% else %}
+ {% set expression = parameter.python_name %}
+ {% endif %}
+ {% set statement = 'headers["' + parameter.name + '"]' + " = " + expression %}
+{{ guarded_statement(parameter, parameter.python_name, statement) }}
+ {% endfor %}
+{% endif %}
+{% endif %}
+{% endmacro %}
+
+{% macro cookie_params(endpoint) %}
+{% if endpoint.cookie_parameters %}
+cookies = {}
+ {% for parameter in endpoint.cookie_parameters %}
+ {% if parameter.required %}
+cookies["{{ parameter.name}}"] = {{ parameter.python_name }}
+ {% else %}
+if {{ parameter.python_name }} is not UNSET:
+ cookies["{{ parameter.name}}"] = {{ parameter.python_name }}
+ {% endif %}
+
+ {% endfor %}
+{% endif %}
+{% endmacro %}
+
+
+{% macro query_params(endpoint) %}
+{% if endpoint.query_parameters %}
+params: dict[str, Any] = {}
+
+{% for property in endpoint.query_parameters %}
+ {% set destination = property.python_name %}
+ {% import "property_templates/" + property.template as prop_template %}
+ {% if prop_template.transform %}
+ {% set destination = "json_" + property.python_name %}
+{{ prop_template.transform(property, property.python_name, destination) }}
+ {% endif %}
+ {%- if not property.json_is_dict %}
+params["{{ property.name }}"] = {{ destination }}
+ {% else %}
+{{ guarded_statement(property, destination, "params.update(" + destination + ")") }}
+ {% endif %}
+
+{% endfor %}
+
+params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+{% endif %}
+{% endmacro %}
+
+{% macro body_to_kwarg(body) %}
+{% if body.body_type == "data" %}
+ {% if body.prop.required %}
+_kwargs["data"] = body.to_dict()
+ {% else %}
+if not isinstance(body, Unset):
+ _kwargs["data"] = body.to_dict()
+ {% endif %}
+{% elif body.body_type == "files"%}
+{{ multipart_body(body) }}
+{% elif body.body_type == "json" %}
+{{ json_body(body) }}
+{% elif body.body_type == "content" %}
+ {% if body.prop.required %}
+_kwargs["content"] = body.payload
+ {% else %}
+if not isinstance(body, Unset):
+ _kwargs["content"] = body.payload
+ {% endif %}
+{% endif %}
+{% endmacro %}
+
+{% macro json_body(body) %}
+{% set property = body.prop %}
+{% import "property_templates/" + property.template as prop_template %}
+{% if prop_template.transform %}
+{{ prop_template.transform(property, property.python_name, "_kwargs[\"json\"]", skip_unset=True, declare_type=False) }}
+{% elif property.required %}
+_kwargs["json"] = {{ property.python_name }}
+{% else %}
+if not isinstance({{property.python_name}}, Unset):
+ _kwargs["json"] = {{ property.python_name }}
+{% endif %}
+{% endmacro %}
+
+{% macro multipart_body(body) %}
+{% set property = body.prop %}
+{% import "property_templates/" + property.template as prop_template %}
+{% if prop_template.transform_multipart_body %}
+{{ prop_template.transform_multipart_body(property) }}
+{% endif %}
+{% endmacro %}
+
+{# Build the full argument list for an endpoint function. #}
+{% macro arguments(endpoint, include_client=True, client_type="httpx.Client") %}
+{# Path parameters come first as positional args. #}
+{% for parameter in endpoint.path_parameters %}
+{{ parameter.to_string() }},
+{% endfor %}
+{#
+ Insert the keyword-only separator (*,) when there's a client arg or any
+ non-path parameters, so path params stay positional while everything
+ else must be passed by name.
+#}
+{% if include_client or ((endpoint.list_all_parameters() | length) > (endpoint.path_parameters | length)) %}
+*,
+{% endif %}
+{% if include_client %}
+client: {{ client_type }},
+{% endif %}
+{# Any allowed bodies #}
+{% if endpoint.bodies | length == 1 %}
+body: {{ endpoint.bodies[0].prop.get_type_string() }}{% if not endpoint.bodies[0].prop.required %} = UNSET{% endif %},
+{% elif endpoint.bodies | length > 1 %}
+body:
+ {%- for body in endpoint.bodies -%}{% set body_required = body_required and body.prop.required %}
+ {{ body.prop.get_type_string(no_optional=True) }} {% if not loop.last %} | {% endif %}
+ {%- endfor -%}{% if not body_required %} | Unset = UNSET{% endif %}
+,
+{% endif %}
+{# query parameters #}
+{% for parameter in endpoint.query_parameters %}
+{{ parameter.to_string() }},
+{% endfor %}
+{% for parameter in endpoint.header_parameters %}
+{{ parameter.to_string() }},
+{% endfor %}
+{# cookie parameters #}
+{% for parameter in endpoint.cookie_parameters %}
+{{ parameter.to_string() }},
+{% endfor %}
+{% endmacro %}
+
+{# Just lists all kwargs to endpoints as name=name for passing to other functions #}
+{% macro kwargs(endpoint, include_client=True) %}
+{% for parameter in endpoint.path_parameters %}
+{{ parameter.python_name }}={{ parameter.python_name }},
+{% endfor %}
+{% if include_client %}
+client=client,
+{% endif %}
+{% if endpoint.bodies | length > 0 %}
+body=body,
+{% endif %}
+{% for parameter in endpoint.query_parameters %}
+{{ parameter.python_name }}={{ parameter.python_name }},
+{% endfor %}
+{% for parameter in endpoint.header_parameters %}
+{{ parameter.python_name }}={{ parameter.python_name }},
+{% endfor %}
+{% for parameter in endpoint.cookie_parameters %}
+{{ parameter.python_name }}={{ parameter.python_name }},
+{% endfor %}
+{% endmacro %}
+
+{% macro docstring_content(endpoint, return_string) %}
+{% if endpoint.summary %}{{ endpoint.summary | wordwrap(100)}}
+
+{% endif -%}
+{%- if endpoint.description %} {{ endpoint.description | wordwrap(100) }}
+
+{% endif %}
+{% if not endpoint.summary and not endpoint.description %}
+{# Leave extra space so that Args or Returns isn't at the top #}
+
+{% endif %}
+{% set all_parameters = endpoint.list_all_parameters() %}
+{% if all_parameters %}
+Args:
+ {% for parameter in all_parameters %}
+ {{ parameter.to_docstring() | wordwrap(90) | indent(8) }}
+ {% endfor %}
+
+{% endif %}
+Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+Returns:
+ {{ return_string }}
+{% endmacro %}
+
+{% macro docstring(endpoint, return_string) %}
+{{ safe_docstring(docstring_content(endpoint, return_string)) }}
+{% endmacro %}
+
+{#
+ Deserialize a single response body. Three strategies:
+ 1. construct: the property template has a custom constructor (e.g. from_dict for models)
+ 2. direct assignment: source type already matches the declared return type
+ 3. cast: types differ at the type-checker level, so wrap in cast()
+ When parsed_responses is false (return type is None), just return None.
+#}
+{% macro parse_response(parsed_responses, response) %}
+{% if parsed_responses %}{% import "property_templates/" + response.prop.template as prop_template %}
+{% if prop_template.construct %}
+{{ prop_template.construct(response.prop, response.source.attribute) }}
+{% elif response.source.return_type == response.prop.get_type_string() %}
+{{ response.prop.python_name }} = {{ response.source.attribute }}
+{% else %}
+{{ response.prop.python_name }} = cast({{ response.prop.get_type_string() }}, {{ response.source.attribute }})
+{% endif %}
+return {{ response.prop.python_name }}
+{% else %}
+return None
+{% endif %}
+{% endmacro %}
+
+{#
+ Derive the Python return-type annotation from the set of success responses.
+ - No responses → None
+ - Single "Any" response (e.g. 204 No Content) → None, since there's nothing
+ meaningful to return and None gives callers a cleaner API
+ - Single typed response → that type
+ - Multiple distinct types → union (T1 | T2)
+#}
+{% macro return_type(responses) %}
+{% set ns = namespace(return_types=[]) %}
+{% for response in responses %}
+ {% if response.prop.get_type_string() not in ns.return_types %}
+ {% set ns.return_types = ns.return_types + [response.prop.get_type_string()] %}
+ {% endif %}
+{% endfor %}
+{%- if ns.return_types | length == 0 -%}
+None
+{%- elif ns.return_types | length == 1 -%}
+ {%- if ns.return_types[0] == 'Any' -%}
+None
+ {%- else -%}
+{{ ns.return_types[0] }}
+ {%- endif -%}
+{%- else -%}
+{{ ns.return_types | sort | join(' | ') }}
+{%- endif -%}
+{%- endmacro %}
\ No newline at end of file
diff --git a/packages/liveblocks-python-codegen/templates/endpoint_module.py.jinja b/packages/liveblocks-python-codegen/templates/endpoint_module.py.jinja
new file mode 100644
index 0000000000..5c9037cc23
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/endpoint_module.py.jinja
@@ -0,0 +1,143 @@
+{% from "_shared.jinja" import hidden_endpoints %}
+{% if endpoint.name not in hidden_endpoints %}
+from typing import Any, cast
+from urllib.parse import quote
+
+import httpx
+
+from ...types import UNSET
+from ... import errors
+
+{#
+ The OpenAPI spec's `deprecated` flag on operations is parsed but never passed
+ to the Endpoint object that templates receive, so `endpoint.deprecated` does
+ not exist. As a workaround, we check for "Deprecated" in the endpoint
+ description, which the Liveblocks API consistently includes on deprecated
+ operations.
+#}
+{% if endpoint.description and "Deprecated" in endpoint.description %}
+import warnings
+{% endif %}
+
+{% for relative in endpoint.relative_imports | sort %}
+{{ relative }}
+{% endfor %}
+
+{% from "endpoint_macros.py.jinja" import header_params, cookie_params, query_params,
+ arguments, client, kwargs, parse_response, body_to_kwarg, return_type %}
+
+{#
+ Collect only 2xx success responses -- these determine the function's return type
+ and which branches _parse_response will generate. We use namespace() because
+ Jinja scoping doesn't allow inner loops to mutate outer variables directly.
+#}
+{% set ns = namespace(success_responses=[]) %}
+{% for response in endpoint.responses.patterns %}
+ {% set code_range = response.status_code.range %}
+ {% if code_range[0] >= 200 and code_range[1] <= 299 %}
+ {% set ns.success_responses = ns.success_responses + [response] %}
+ {% endif %}
+{% endfor %}
+
+{%- set return_string = return_type(ns.success_responses) -%}
+
+def _get_kwargs(
+ {{ arguments(endpoint, include_client=False) | indent(4) }}
+) -> dict[str, Any]:
+ {{ header_params(endpoint) | indent(4) }}
+
+ {{ cookie_params(endpoint) | indent(4) }}
+
+ {{ query_params(endpoint) | indent(4) }}
+
+ _kwargs: dict[str, Any] = {
+ "method": "{{ endpoint.method }}",
+ {% if endpoint.path_parameters %}
+ "url": "/v2{{ endpoint.path }}".format(
+ {%- for parameter in endpoint.path_parameters -%}
+ {{parameter.python_name}}=quote(str({{parameter.python_name}}), safe=""),
+ {%- endfor -%}
+ ),
+ {% else %}
+ "url": "/v2{{ endpoint.path }}",
+ {% endif %}
+ {% if endpoint.query_parameters %}
+ "params": params,
+ {% endif %}
+ {% if endpoint.cookie_parameters %}
+ "cookies": cookies,
+ {% endif %}
+ }
+
+{% if endpoint.bodies | length > 1 %}
+{% for body in endpoint.bodies %}
+ if isinstance(body, {{body.prop.get_type_string(no_optional=True) }}):
+ {{ body_to_kwarg(body) | indent(8) }}
+ headers["Content-Type"] = "{{ body.content_type }}"
+{% endfor %}
+{% elif endpoint.bodies | length == 1 %}
+{% set body = endpoint.bodies[0] %}
+ {{ body_to_kwarg(body) | indent(4) }}
+ {% if body.content_type != "multipart/form-data" %}{# Need httpx to set the boundary automatically #}
+ headers["Content-Type"] = "{{ body.content_type }}"
+ {% endif %}
+{% endif %}
+
+{% if endpoint.header_parameters or endpoint.bodies | length > 0 %}
+ _kwargs["headers"] = headers
+{% endif %}
+ return _kwargs
+
+{# Match each 2xx status code and deserialize the body; anything else is an error. #}
+def _parse_response(*, response: httpx.Response) -> {{ return_string }}:
+ {% for response in ns.success_responses %}
+ {% set code_range = response.status_code.range %}
+ {% if code_range[0] == code_range[1] %}
+ if response.status_code == {{ code_range[0] }}:
+ {% else %}
+ if {{ code_range[0] }} <= response.status_code <= {{ code_range[1] }}:
+ {% endif %}
+ {{ parse_response(return_string != "None", response) | indent(8) }}
+ {% endfor %}
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ {{ arguments(endpoint, client_type="httpx.Client") | indent(4) }}
+) -> {{ return_string }}:
+{% if endpoint.description and "Deprecated" in endpoint.description %}
+ warnings.warn(
+ "{{ endpoint.name | snakecase }} is deprecated",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+{% endif %}
+ kwargs = _get_kwargs(
+ {{ kwargs(endpoint, include_client=False) }}
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+async def _asyncio(
+ {{ arguments(endpoint, client_type="httpx.AsyncClient") | indent(4) }}
+) -> {{ return_string }}:
+{% if endpoint.description and "Deprecated" in endpoint.description %}
+ warnings.warn(
+ "{{ endpoint.name | snakecase }} is deprecated",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+{% endif %}
+ kwargs = _get_kwargs(
+ {{ kwargs(endpoint, include_client=False) }}
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
+{% endif %}
diff --git a/packages/liveblocks-python-codegen/templates/errors.py.jinja b/packages/liveblocks-python-codegen/templates/errors.py.jinja
new file mode 100644
index 0000000000..12f8e3ca92
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/errors.py.jinja
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+import json
+from typing import Any
+import httpx
+
+class LiveblocksError(Exception):
+
+ def __init__(self, message: str, status: int, details: str | None = None):
+ super().__init__(message)
+ self.status = status
+ self.details = details
+
+ def __str__(self) -> str:
+ msg = f"{self.args[0]} (status {self.status})"
+ if self.details:
+ msg += f"\n{self.details}"
+ return msg
+
+ @classmethod
+ def from_response(cls, response: httpx.Response) -> "LiveblocksError":
+ FALLBACK = "An error happened without an error message"
+ try:
+ response.read()
+ text = response.text
+ except Exception:
+ text = FALLBACK
+
+ obj: dict[str, Any]
+ try:
+ parsed = json.loads(text)
+ if isinstance(parsed, dict):
+ obj = parsed
+ else:
+ obj = {"message": text}
+ except Exception:
+ obj = {"message": text}
+
+ message = str(obj.get("message") or FALLBACK)
+
+ parts: list[str] = []
+ if obj.get("suggestion") is not None:
+ parts.append(f"Suggestion: {obj['suggestion']}")
+ if obj.get("docs") is not None:
+ parts.append(f"See also: {obj['docs']}")
+
+ details = "\n".join(parts) or None
+ return cls(message, response.status_code, details)
+
+__all__ = ["LiveblocksError"]
\ No newline at end of file
diff --git a/packages/liveblocks-python-codegen/templates/model.py.jinja b/packages/liveblocks-python-codegen/templates/model.py.jinja
new file mode 100644
index 0000000000..1bea368c2c
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/model.py.jinja
@@ -0,0 +1,256 @@
+{% from "_shared.jinja" import hidden_models %}
+{% if model.class_info.name not in hidden_models %}
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, BinaryIO, TextIO, TYPE_CHECKING, Generator
+{% if model.data.deprecated %}
+import warnings
+{% endif %}
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+{% if model.is_multipart_body %}
+import json
+from .. import types
+{% endif %}
+
+from ..types import UNSET, Unset
+
+{% for relative in model.relative_imports | sort %}
+{{ relative }}
+{% endfor %}
+
+{% for lazy_import in model.lazy_imports | sort %}
+{% if loop.first %}
+if TYPE_CHECKING:
+{% endif %}
+ {{ lazy_import }}
+{% endfor %}
+
+
+{% if model.additional_properties %}
+{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string() %}
+{% endif %}
+
+{% set class_name = model.class_info.name %}
+{% set module_name = model.class_info.module_name %}
+
+{% from "helpers.jinja" import safe_docstring %}
+
+{% macro class_docstring_content(model) %}
+ {% if model.title %}{{ model.title | wordwrap(116) }}
+
+ {% endif -%}
+ {%- if model.description %}{{ model.description | wordwrap(116) }}
+
+ {% endif %}
+ {% if not model.title and not model.description %}
+ {# Leave extra space so that a section doesn't start on the first line #}
+
+ {% endif %}
+ {% if model.example %}
+ Example:
+ {{ model.example | string | wordwrap(112) | indent(12) }}
+
+ {% endif %}
+ {% if (not config.docstrings_on_attributes) and (model.required_properties or model.optional_properties) %}
+ Attributes:
+ {% for property in model.required_properties + model.optional_properties %}
+ {{ property.to_docstring() | wordwrap(112) | indent(12) }}
+ {% endfor %}{% endif %}
+{% endmacro %}
+
+{% macro declare_property(property) %}
+{%- if config.docstrings_on_attributes and property.description -%}
+{{ property.to_string() }}
+{{ safe_docstring(property.description, omit_if_empty=True) | wordwrap(112) }}
+{%- else -%}
+{{ property.to_string() }}
+{%- endif -%}
+{% endmacro %}
+
+@_attrs_define
+class {{ class_name }}:
+ {{ safe_docstring(class_docstring_content(model), omit_if_empty=config.docstrings_on_attributes) | indent(4) }}
+
+ {% for property in model.required_properties + model.optional_properties %}
+ {% if property.default is none and property.required %}
+ {{ declare_property(property) | indent(4) }}
+ {% endif %}
+ {% endfor %}
+ {% for property in model.required_properties + model.optional_properties %}
+ {% if property.default is not none or not property.required %}
+ {{ declare_property(property) | indent(4) }}
+ {% endif %}
+ {% endfor %}
+ {% if model.additional_properties %}
+ additional_properties: dict[str, {{ additional_property_type }}] = _attrs_field(init=False, factory=dict)
+ {% endif %}
+
+{% if model.data.deprecated %}
+ def __attrs_post_init__(self) -> None:
+ warnings.warn(
+ "{{ class_name }} is deprecated",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+{% endif %}
+
+{% macro _transform_property(property, content) %}
+{% import "property_templates/" + property.template as prop_template %}
+{%- if prop_template.transform -%}
+{{ prop_template.transform(property=property, source=content, destination=property.python_name) }}
+{%- else -%}
+{{ property.python_name }} = {{ content }}
+{%- endif -%}
+{% endmacro %}
+
+{% macro multipart(property, source, destination) %}
+{% import "property_templates/" + property.template as prop_template %}
+{% if not property.required %}
+if not isinstance({{source}}, Unset):
+ {{ prop_template.multipart(property, source, destination) | indent(4) }}
+{% else %}
+{{ prop_template.multipart(property, source, destination) }}
+{% endif %}
+{% endmacro %}
+
+{% macro _prepare_field_dict() %}
+field_dict: dict[str, Any] = {}
+{% if model.additional_properties %}
+{% import "property_templates/" + model.additional_properties.template as prop_template %}
+{% if prop_template.transform %}
+for prop_name, prop in self.additional_properties.items():
+ {{ prop_template.transform(model.additional_properties, "prop", "field_dict[prop_name]", declare_type=false) | indent(4) }}
+{% else %}
+field_dict.update(self.additional_properties)
+{%- endif -%}
+{%- endif -%}
+{% endmacro %}
+
+{% macro _to_dict() %}
+{% for property in model.required_properties + model.optional_properties -%}
+{{ _transform_property(property, "self." + property.python_name) }}
+
+{% endfor %}
+
+{{ _prepare_field_dict() }}
+{% if model.required_properties | length > 0 or model.optional_properties | length > 0 %}
+field_dict.update({
+ {% for property in model.required_properties + model.optional_properties %}
+ {% if property.required %}
+ "{{ property.name }}": {{ property.python_name }},
+ {% endif %}
+ {% endfor %}
+})
+{% endif %}
+{% for property in model.optional_properties %}
+{% if not property.required %}
+if {{ property.python_name }} is not UNSET:
+ field_dict["{{ property.name }}"] = {{ property.python_name }}
+{% endif %}
+{% endfor %}
+
+return field_dict
+{% endmacro %}
+
+ def to_dict(self) -> dict[str, Any]:
+ {% for lazy_import in model.lazy_imports | sort %}
+ {{ lazy_import }}
+ {% endfor %}
+ {{ _to_dict() | indent(8) }}
+
+{% if model.is_multipart_body %}
+ def to_multipart(self) -> types.RequestFiles:
+ {% for lazy_import in model.lazy_imports | sort %}
+ {{ lazy_import }}
+ {% endfor %}
+ files: types.RequestFiles = []
+
+ {% for property in model.required_properties + model.optional_properties %}
+ {% set destination = "\"" + property.name + "\"" %}
+ {{ multipart(property, "self." + property.python_name, destination) | indent(8) }}
+
+ {% endfor %}
+
+ {% if model.additional_properties %}
+ for prop_name, prop in self.additional_properties.items():
+ {{ multipart(model.additional_properties, "prop", "prop_name") | indent(4) }}
+ {% endif %}
+
+ return files
+
+{% endif %}
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ {% for lazy_import in model.lazy_imports | sort %}
+ {{ lazy_import }}
+ {% endfor %}
+{% if (model.required_properties or model.optional_properties or model.additional_properties) %}
+ d = dict(src_dict)
+{% for property in model.required_properties + model.optional_properties %}
+ {% if property.required %}
+ {% set property_source = 'd.pop("' + property.name + '")' %}
+ {% else %}
+ {% set property_source = 'd.pop("' + property.name + '", UNSET)' %}
+ {% endif %}
+ {% import "property_templates/" + property.template as prop_template %}
+ {% if prop_template.construct %}
+ {{ prop_template.construct(property, property_source) | indent(8) }}
+ {% else %}
+ {{ property.python_name }} = {{ property_source }}
+ {% endif %}
+
+{% endfor %}
+{% endif %}
+ {{ module_name }} = cls(
+{% for property in model.required_properties + model.optional_properties %}
+ {{ property.python_name }}={{ property.python_name }},
+{% endfor %}
+ )
+
+{% if model.additional_properties %}
+ {% if model.additional_properties.template %}{# Can be a bool instead of an object #}
+ {% import "property_templates/" + model.additional_properties.template as prop_template %}
+
+{% if model.additional_properties.lazy_imports %}
+ {% for lazy_import in model.additional_properties.lazy_imports | sort %}
+ {{ lazy_import }}
+ {% endfor %}
+{% endif %}
+ {% else %}
+ {% set prop_template = None %}
+ {% endif %}
+ {% if prop_template and prop_template.construct %}
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+ {{ prop_template.construct(model.additional_properties, "prop_dict") | indent(12) }}
+ additional_properties[prop_name] = {{ model.additional_properties.python_name }}
+
+ {{ module_name }}.additional_properties = additional_properties
+ {% else %}
+ {{ module_name }}.additional_properties = d
+ {% endif %}
+{% endif %}
+ return {{ module_name }}
+
+ {% if model.additional_properties %}
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> {{ additional_property_type }}:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: {{ additional_property_type }}) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
+ {% endif %}
+{% endif %}
diff --git a/packages/liveblocks-python-codegen/templates/models_init.py.jinja b/packages/liveblocks-python-codegen/templates/models_init.py.jinja
new file mode 100644
index 0000000000..e124aa7c87
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/models_init.py.jinja
@@ -0,0 +1,26 @@
+""" Contains all the data models used in inputs/outputs """
+
+{% from "_shared.jinja" import hidden_models %}
+
+{# Emit all model imports except those listed in hidden_models (see _shared.jinja). #}
+{% for import in imports | sort %}
+{% set ns = namespace(is_hidden=false) %}
+{% for hidden in hidden_models %}
+{% if hidden in import %}
+{% set ns.is_hidden = true %}
+{% endif %}
+{% endfor %}
+{% if not ns.is_hidden %}
+{{ import }}
+{% endif %}
+{% endfor %}
+
+{% if imports %}
+__all__ = (
+ {% for all in alls | sort %}
+ {% if all not in hidden_models %}
+ "{{ all }}",
+ {% endif %}
+ {% endfor %}
+)
+{% endif %}
diff --git a/packages/liveblocks-python-codegen/templates/package_init.py.jinja b/packages/liveblocks-python-codegen/templates/package_init.py.jinja
new file mode 100644
index 0000000000..1f510c86fa
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/package_init.py.jinja
@@ -0,0 +1,13 @@
+{% from "helpers.jinja" import safe_docstring %}
+
+{{ safe_docstring(package_description) }}
+from .client import AsyncLiveblocks, Liveblocks
+from .webhooks import WebhookHandler
+from .errors import LiveblocksError
+
+__all__ = (
+ "AsyncLiveblocks",
+ "Liveblocks",
+ "LiveblocksError",
+ "WebhookHandler",
+)
diff --git a/packages/liveblocks-python-codegen/templates/pyproject_ruff.toml.jinja b/packages/liveblocks-python-codegen/templates/pyproject_ruff.toml.jinja
new file mode 100644
index 0000000000..ae310b2055
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/pyproject_ruff.toml.jinja
@@ -0,0 +1,6 @@
+[tool.ruff]
+line-length = 120
+target-version = "py311"
+
+[tool.ruff.lint]
+select = ["F", "I", "UP"]
\ No newline at end of file
diff --git a/packages/liveblocks-python-codegen/templates/pyproject_uv.toml.jinja b/packages/liveblocks-python-codegen/templates/pyproject_uv.toml.jinja
new file mode 100644
index 0000000000..f8e0ebf792
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/pyproject_uv.toml.jinja
@@ -0,0 +1,54 @@
+[project]
+name = "{{ project_name }}"
+version = "{{ package_version }}"
+description = "{{ package_description }}"
+authors = []
+requires-python = ">=3.11"
+readme = "README.md"
+dependencies = [
+ "httpx>=0.23.0,<0.29.0",
+ "attrs>=22.2.0",
+ "python-dateutil>=2.8.0,<3",
+]
+
+[dependency-groups]
+dev = [
+ "pytest>=9.0",
+ "pytest-cov>=7.0",
+ "respx>=0.22.0",
+]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+
+[tool.coverage.run]
+source = ["liveblocks"]
+omit = [
+ "liveblocks/models/*",
+ "liveblocks/api/*",
+]
+
+[tool.coverage.report]
+show_missing = true
+fail_under = 0
+# Exclude the delegation boilerplate inside client.py methods.
+# Each public method on Liveblocks/AsyncLiveblocks just imports an
+# API module and forwards the call — the real logic lives in
+# liveblocks/api/* (already omitted above). These patterns skip:
+# 1. The local import line (e.g. `from .api.room import get_rooms`)
+# 2. The sync return line (e.g. `return get_rooms._sync(...)`)
+# 3. The async return line (e.g. `return await get_rooms._asyncio(...)`)
+exclude_also = [
+ "from \\.api\\.",
+ "return .*\\._sync\\(",
+ "return await .*\\._asyncio\\(",
+]
+
+[tool.uv.build-backend]
+module-name = "{{ package_name }}"
+module-root = ""
+exclude = ["README.mdx"]
+
+[build-system]
+requires = ["uv_build>=0.10.0,<0.11.0"]
+build-backend = "uv_build"
diff --git a/packages/liveblocks-python-codegen/templates/str_enum.py.jinja b/packages/liveblocks-python-codegen/templates/str_enum.py.jinja
new file mode 100644
index 0000000000..c408fb00a1
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/str_enum.py.jinja
@@ -0,0 +1,6 @@
+from enum import StrEnum
+
+class {{ enum.class_info.name }}(StrEnum):
+ {% for key, value in enum.values|dictsort(true) %}
+ {{ key }} = "{{ value }}"
+ {% endfor %}
diff --git a/packages/liveblocks-python-codegen/templates/types.py.jinja b/packages/liveblocks-python-codegen/templates/types.py.jinja
new file mode 100644
index 0000000000..cf4ba834e7
--- /dev/null
+++ b/packages/liveblocks-python-codegen/templates/types.py.jinja
@@ -0,0 +1,39 @@
+""" Contains some shared types for properties """
+
+from collections.abc import Mapping, MutableMapping
+from http import HTTPStatus
+from typing import BinaryIO, Generic, TypeVar, Literal, IO
+
+from attrs import define
+
+
+class Unset:
+ def __bool__(self) -> Literal[False]:
+ return False
+
+
+UNSET: Unset = Unset()
+
+# The types that `httpx.Client(files=)` can accept, copied from that library.
+FileContent = IO[bytes] | bytes | str
+FileTypes = (
+ # (filename, file (or bytes), content_type)
+ tuple[str | None, FileContent, str | None]
+ # (filename, file (or bytes), content_type, headers)
+ | tuple[str | None, FileContent, str | None, Mapping[str, str]]
+)
+
+@define
+class File:
+ """ Contains information for file uploads """
+
+ payload: BinaryIO
+ file_name: str | None = None
+ mime_type: str | None = None
+
+ def to_tuple(self) -> FileTypes:
+ """ Return a tuple representation that httpx will accept for multipart/form-data """
+ return self.file_name, self.payload, self.mime_type
+
+
+__all__ = ["UNSET", "File", "FileTypes", "Unset"]
diff --git a/packages/liveblocks-python-codegen/uv.lock b/packages/liveblocks-python-codegen/uv.lock
new file mode 100644
index 0000000000..0287994986
--- /dev/null
+++ b/packages/liveblocks-python-codegen/uv.lock
@@ -0,0 +1,455 @@
+version = 1
+revision = 3
+requires-python = ">=3.12"
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.2.25"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "liveblocks-python-codegen"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "openapi-python-client" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "openapi-python-client", specifier = "==0.28.2" }]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
+ { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
+ { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "openapi-python-client"
+version = "0.28.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "httpx" },
+ { name = "jinja2" },
+ { name = "pydantic" },
+ { name = "python-dateutil" },
+ { name = "ruamel-yaml" },
+ { name = "ruff" },
+ { name = "shellingham" },
+ { name = "typer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/78/3ecdf2a6fc49cabb09c75c20df2cfa8756d360409093f7f9f8ee5c23984d/openapi_python_client-0.28.2.tar.gz", hash = "sha256:3699a7138fae3d89ce7f27922a985562e88fe790171db5626bfc7b4ae4cd4c31", size = 125955, upload-time = "2026-02-12T01:22:51.583Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/1b/10f47043498f16c4f6a0492d2d309c2b42599f6cd7440adab7e88b1c0d3a/openapi_python_client-0.28.2-py3-none-any.whl", hash = "sha256:0b6b84114af49ca672a57ae4421db9b2b09f55304a525731b91cf9d80e9313e5", size = 183194, upload-time = "2026-02-12T01:22:49.629Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
+]
+
+[[package]]
+name = "ruamel-yaml"
+version = "0.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.15.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/77/9b/840e0039e65fcf12758adf684d2289024d6140cde9268cc59887dc55189c/ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2", size = 4574214, upload-time = "2026-03-05T20:06:34.946Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/20/5369c3ce21588c708bcbe517a8fbe1a8dfdb5dfd5137e14790b1da71612c/ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c", size = 10478185, upload-time = "2026-03-05T20:06:29.093Z" },
+ { url = "https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080", size = 10859201, upload-time = "2026-03-05T20:06:32.632Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010", size = 10184752, upload-time = "2026-03-05T20:06:40.312Z" },
+ { url = "https://files.pythonhosted.org/packages/66/0e/ba49e2c3fa0395b3152bad634c7432f7edfc509c133b8f4529053ff024fb/ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65", size = 10534857, upload-time = "2026-03-05T20:06:19.581Z" },
+ { url = "https://files.pythonhosted.org/packages/59/71/39234440f27a226475a0659561adb0d784b4d247dfe7f43ffc12dd02e288/ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440", size = 10309120, upload-time = "2026-03-05T20:06:00.435Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/87/4140aa86a93df032156982b726f4952aaec4a883bb98cb6ef73c347da253/ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204", size = 11047428, upload-time = "2026-03-05T20:05:51.867Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f7/4953e7e3287676f78fbe85e3a0ca414c5ca81237b7575bdadc00229ac240/ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8", size = 11914251, upload-time = "2026-03-05T20:06:22.887Z" },
+ { url = "https://files.pythonhosted.org/packages/77/46/0f7c865c10cf896ccf5a939c3e84e1cfaeed608ff5249584799a74d33835/ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681", size = 11333801, upload-time = "2026-03-05T20:05:57.168Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/01/a10fe54b653061585e655f5286c2662ebddb68831ed3eaebfb0eb08c0a16/ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a", size = 11206821, upload-time = "2026-03-05T20:06:03.441Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/0d/2132ceaf20c5e8699aa83da2706ecb5c5dcdf78b453f77edca7fb70f8a93/ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca", size = 11133326, upload-time = "2026-03-05T20:06:25.655Z" },
+ { url = "https://files.pythonhosted.org/packages/72/cb/2e5259a7eb2a0f87c08c0fe5bf5825a1e4b90883a52685524596bfc93072/ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd", size = 10510820, upload-time = "2026-03-05T20:06:37.79Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/20/b67ce78f9e6c59ffbdb5b4503d0090e749b5f2d31b599b554698a80d861c/ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d", size = 10302395, upload-time = "2026-03-05T20:05:54.504Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e5/719f1acccd31b720d477751558ed74e9c88134adcc377e5e886af89d3072/ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752", size = 10754069, upload-time = "2026-03-05T20:06:06.422Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/9c/d1db14469e32d98f3ca27079dbd30b7b44dbb5317d06ab36718dee3baf03/ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2", size = 11304315, upload-time = "2026-03-05T20:06:10.867Z" },
+ { url = "https://files.pythonhosted.org/packages/28/3a/950367aee7c69027f4f422059227b290ed780366b6aecee5de5039d50fa8/ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74", size = 10551676, upload-time = "2026-03-05T20:06:13.705Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/00/bf077a505b4e649bdd3c47ff8ec967735ce2544c8e4a43aba42ee9bf935d/ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe", size = 11678972, upload-time = "2026-03-05T20:06:45.379Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572, upload-time = "2026-03-05T20:06:16.984Z" },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "typer"
+version = "0.23.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d3/ae/93d16574e66dfe4c2284ffdaca4b0320ade32858cb2cc586c8dd79f127c5/typer-0.23.2.tar.gz", hash = "sha256:a99706a08e54f1aef8bb6a8611503808188a4092808e86addff1828a208af0de", size = 120162, upload-time = "2026-02-16T18:52:40.354Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/2c/dee705c427875402200fe779eb8a3c00ccb349471172c41178336e9599cc/typer-0.23.2-py3-none-any.whl", hash = "sha256:e9c8dc380f82450b3c851a9b9d5a0edf95d1d6456ae70c517d8b06a50c7a9978", size = 56834, upload-time = "2026-02-16T18:52:39.308Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
diff --git a/packages/liveblocks-python/.gitignore b/packages/liveblocks-python/.gitignore
new file mode 100644
index 0000000000..79a2c3d73c
--- /dev/null
+++ b/packages/liveblocks-python/.gitignore
@@ -0,0 +1,23 @@
+__pycache__/
+build/
+dist/
+*.egg-info/
+.pytest_cache/
+
+# pyenv
+.python-version
+
+# Environments
+.env
+.venv
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# JetBrains
+.idea/
+
+/coverage.xml
+/.coverage
diff --git a/packages/liveblocks-python/README.md b/packages/liveblocks-python/README.md
new file mode 100644
index 0000000000..3fc1e674d6
--- /dev/null
+++ b/packages/liveblocks-python/README.md
@@ -0,0 +1,2521 @@
+# @liveblocks/python
+
+`@liveblocks/python` provides you with a Python client for accessing the Liveblocks API. This library is only intended for use in your Python back end.
+
+## Installation
+
+Install the Liveblocks package to get started.
+
+```bash
+pip install liveblocks
+```
+
+## Quickstart
+
+All API calls require a Liveblocks client set up with your secret key. Find your key in the [Liveblocks Dashboard](https://liveblocks.io/dashboard/apikeys).
+
+### Synchronous
+
+```python
+from liveblocks import Liveblocks
+
+client = Liveblocks(secret="{{SECRET_KEY}}")
+
+with client:
+ rooms = client.get_rooms()
+ print(rooms)
+```
+
+### Asynchronous
+
+```python
+from liveblocks import AsyncLiveblocks
+
+client = AsyncLiveblocks(secret="{{SECRET_KEY}}")
+
+async with client:
+ rooms = await client.get_rooms()
+ print(rooms)
+```
+
+---
+
+## API Reference
+
+### Room
+
+#### `get_rooms`
+
+This endpoint returns a list of your rooms. The rooms are returned sorted by creation date, from newest to oldest. You can filter rooms by room ID prefixes, metadata, users accesses, and groups accesses. Corresponds to [`liveblocks.getRooms`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms).
+
+There is a pagination system where the cursor to the next page is returned in the response as `nextCursor`, which can be combined with `startingAfter`.
+You can also limit the number of rooms by query.
+
+Filtering by metadata works by giving key values like `metadata.color=red`. Of course you can combine multiple metadata clauses to refine the response like `metadata.color=red&metadata.type=text`. Notice here the operator AND is applied between each clauses.
+
+Filtering by groups or userId works by giving a list of groups like `groupIds=marketing,GZo7tQ,product` or/and a userId like `userId=user1`.
+Notice here the operator OR is applied between each `groupIds` and the `userId`.
+
+
+**Example**
+```python
+result = client.get_rooms(
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+ # organization_id="org_123456789",
+ # query="metadata[\"color\"]:\"blue\"",
+ # user_id="user-123",
+ # group_ids="group1,group2",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `limit` | `int \| Unset` | No | A limit on the number of rooms to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)* |
+| `starting_after` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+| `organization_id` | `str \| Unset` | No | A filter on organization ID. |
+| `query` | `str \| Unset` | No | Query to filter rooms. You can filter by `roomId` and `metadata`, for example, `metadata["roomType"]:"whiteboard" AND roomId^"liveblocks:engineering"`. Learn more about [filtering rooms with query language](https://liveblocks.io/docs/guides/how-to-filter-rooms-using-query-language). |
+| `user_id` | `str \| Unset` | No | A filter on users accesses. |
+| `group_ids` | `str \| Unset` | No | A filter on groups accesses. Multiple groups can be used. |
+
+
+---
+
+#### `create_room`
+
+This endpoint creates a new room. `id` and `defaultAccesses` are required. When provided with a `?idempotent` query argument, will not return a 409 when the room already exists, but instead return the existing room as-is. Corresponds to [`liveblocks.createRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms), or to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-create-rooms-roomId) when `?idempotent` is provided.
+- `defaultAccesses` could be `[]` or `["room:write"]` (private or public).
+- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.
+- `usersAccesses` could be `[]` or `["room:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+- `groupsAccesses` are optional fields.
+
+
+**Example**
+```python
+from liveblocks.models import CreateRoomRequestBody
+
+result = client.create_room(
+ body=CreateRoomRequestBody(
+ id="...",
+ default_accesses=[],
+ # organization_id="...",
+ # users_accesses=...,
+ # groups_accesses=...,
+ ),
+ # idempotent=True,
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `idempotent` | `bool \| Unset` | No | When provided, will not return a 409 when the room already exists, but instead return the existing room as-is. Corresponds to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-create-rooms-roomId). |
+| `body` | `CreateRoomRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `get_room`
+
+This endpoint returns a room by its ID. Corresponds to [`liveblocks.getRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomid).
+
+**Example**
+```python
+result = client.get_room(
+ room_id="my-room-id",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+
+
+---
+
+#### `update_room`
+
+This endpoint updates specific properties of a room. Corresponds to [`liveblocks.updateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomid).
+
+It’s not necessary to provide the entire room’s information.
+Setting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users:
+``{
+ "usersAccesses": {
+ "john": null
+ }
+}``
+`defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.
+
+- `defaultAccesses` could be `[]` or `["room:write"]` (private or public).
+- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.
+- `usersAccesses` could be `[]` or `["room:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+- `groupsAccesses` could be `[]` or `["room:write"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field.
+
+**Example**
+```python
+from liveblocks.models import UpdateRoomRequestBody
+
+result = client.update_room(
+ room_id="my-room-id",
+ body=UpdateRoomRequestBody(
+ # default_accesses=[],
+ # users_accesses=...,
+ # groups_accesses=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `body` | `UpdateRoomRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `delete_room`
+
+This endpoint deletes a room. A deleted room is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.deleteRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomid).
+
+**Example**
+```python
+client.delete_room(
+ room_id="my-room-id",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+
+
+---
+
+#### `prewarm_room`
+
+Speeds up connecting to a room for the next 10 seconds. Use this when you know a user will be connecting to a room with [`RoomProvider`](https://liveblocks.io/docs/api-reference/liveblocks-react#RoomProvider) or [`enterRoom`](https://liveblocks.io/docs/api-reference/liveblocks-client#Client.enterRoom) within 10 seconds, and the room will load quicker. Corresponds to [`liveblocks.prewarmRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomid-prewarm).
+
+**Example**
+```python
+client.prewarm_room(
+ room_id="my-room-id",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+
+
+---
+
+#### `upsert_room`
+
+This endpoint updates specific properties of a room. Corresponds to [`liveblocks.upsertRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#upsert-rooms-roomId).
+
+It’s not necessary to provide the entire room’s information.
+Setting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users:
+``{
+ "usersAccesses": {
+ "john": null
+ }
+}``
+`defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.
+
+- `defaultAccesses` could be `[]` or `["room:write"]` (private or public).
+- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.
+- `usersAccesses` could be `[]` or `["room:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+- `groupsAccesses` could be `[]` or `["room:write"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field.
+
+**Example**
+```python
+from liveblocks.models import UpsertRoomRequestBody
+
+result = client.upsert_room(
+ room_id="my-room-id",
+ body=UpsertRoomRequestBody(
+ update=...,
+ # create=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `body` | `UpsertRoomRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `update_room_id`
+
+This endpoint permanently updates the room’s ID. All existing references to the old room ID will need to be updated. Returns the updated room. Corresponds to [`liveblocks.updateRoomId`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomid-update-room-id).
+
+**Example**
+```python
+result = client.update_room_id(
+ room_id="my-room-id",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | The new ID for the room |
+| `body` | `UpdateRoomIdRequestBody \| Unset` | No | Request body (application/json) |
+
+
+---
+
+#### `get_active_users`
+
+This endpoint returns a list of users currently present in the requested room. Corresponds to [`liveblocks.getActiveUsers`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomid-active-users).
+
+For optimal performance, we recommend calling this endpoint no more than once every 10 seconds.
+Duplicates can occur if a user is in the requested room with multiple browser tabs opened.
+
+**Example**
+```python
+result = client.get_active_users(
+ room_id="my-room-id",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+
+
+---
+
+#### `set_presence`
+
+This endpoint sets ephemeral presence for a user in a room without requiring a WebSocket connection. The presence data will automatically expire after the specified TTL (time-to-live). This is useful for scenarios like showing an AI agent's presence in a room. The presence will be broadcast to all connected users in the room. Corresponds to [`liveblocks.setPresence`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-presence).
+
+**Example**
+```python
+from liveblocks.models import SetPresenceRequestBody
+
+client.set_presence(
+ room_id="my-room-id",
+ body=SetPresenceRequestBody(
+ user_id="...",
+ data=...,
+ # user_info=...,
+ # ttl=0,
+ ),
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `body` | `SetPresenceRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `broadcast_event`
+
+This endpoint enables the broadcast of an event to a room without having to connect to it via the `client` from `@liveblocks/client`. It takes any valid JSON as a request body. The `connectionId` passed to event listeners is `-1` when using this API. Corresponds to [`liveblocks.broadcastEvent`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-broadcast-event).
+
+**Example**
+```python
+client.broadcast_event(
+ room_id="my-room-id",
+ body=...,
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `body` | `Any` | Yes | Request body (application/json) |
+
+
+---
+
+### Storage
+
+#### `get_storage_document`
+
+Returns the contents of the room’s Storage tree. Corresponds to [`liveblocks.getStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-storage).
+
+The default outputted format is called “plain LSON”, which includes information on the Live data structures in the tree. These nodes show up in the output as objects with two properties, for example:
+
+```json
+{
+ "liveblocksType": "LiveObject",
+ "data": ...
+}
+```
+
+If you’re not interested in this information, you can use the simpler `?format=json` query param, see below.
+
+**Example**
+```python
+result = client.get_storage_document(
+ room_id="my-room-id",
+ # format_=...,
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `format_` | `GetStorageDocumentFormat \| Unset` | No | Use the `json` format to output a simplified JSON representation of the Storage tree. In that format, each LiveObject and LiveMap will be formatted as a simple JSON object, and each LiveList will be formatted as a simple JSON array. This is a lossy format because information about the original data structures is not retained, but it may be easier to work with. |
+
+
+---
+
+#### `initialize_storage_document`
+
+This endpoint initializes or reinitializes a room’s Storage. The room must already exist. Calling this endpoint will disconnect all users from the room if there are any, triggering a reconnect. Corresponds to [`liveblocks.initializeStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-storage).
+
+The format of the request body is the same as what’s returned by the get Storage endpoint.
+
+For each Liveblocks data structure that you want to create, you need a JSON element having two properties:
+- `"liveblocksType"` => `"LiveObject" | "LiveList" | "LiveMap"`
+- `"data"` => contains the nested data structures (children) and data.
+
+The root’s type can only be LiveObject.
+
+A utility function, `toPlainLson` is included in `@liveblocks/client` from `1.0.9` to help convert `LiveObject`, `LiveList`, and `LiveMap` to the structure expected by the endpoint.
+
+**Example**
+```python
+result = client.initialize_storage_document(
+ room_id="my-room-id",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `body` | `InitializeStorageDocumentBody \| Unset` | No | Request body (application/json) |
+
+
+---
+
+#### `delete_storage_document`
+
+This endpoint deletes all of the room’s Storage data. Calling this endpoint will disconnect all users from the room if there are any. Corresponds to [`liveblocks.deleteStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-storage).
+
+
+**Example**
+```python
+client.delete_storage_document(
+ room_id="my-room-id",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+
+
+---
+
+#### `patch_storage_document`
+
+Applies a sequence of [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations to the room's Storage document, useful for modifying Storage. Operations are applied in order; if any operation fails, the document is not changed and a 422 response with a helpful message is returned.
+
+**Paths and data types:** Be as specific as possible with your target path. Every parent in the chain of path segments must be a LiveObject, LiveList, or LiveMap. Complex nested objects passed in `add` or `replace` operations are automatically converted to LiveObjects and LiveLists.
+
+**Performance:** For large Storage documents, applying a patch can be expensive because the full state is reconstructed on the server to apply the operations. Very large documents may not be suitable for this endpoint.
+
+For a **full guide with examples**, see [Modifying storage via REST API with JSON Patch](https://liveblocks.io/docs/guides/modifying-storage-via-rest-api-with-json-patch).
+
+**Example**
+```python
+client.patch_storage_document(
+ room_id="my-room-id",
+ body=...,
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `body` | `list[AddJsonPatchOperation \| CopyJsonPatchOperation \| MoveJsonPatchOperation \| RemoveJsonPatchOperation \| ReplaceJsonPatchOperation \| TestJsonPatchOperation]` | Yes | Request body (application/json) |
+
+
+---
+
+### Yjs
+
+#### `get_yjs_document`
+
+This endpoint returns a JSON representation of the room’s Yjs document. Corresponds to [`liveblocks.getYjsDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc).
+
+**Example**
+```python
+result = client.get_yjs_document(
+ room_id="my-room-id",
+ # formatting=True,
+ # key="root",
+ # type_=...,
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `formatting` | `bool \| Unset` | No | If present, YText will return formatting. |
+| `key` | `str \| Unset` | No | Returns only a single key’s value, e.g. `doc.get(key).toJSON()`. |
+| `type_` | `GetYjsDocumentType \| Unset` | No | Used with key to override the inferred type, i.e. `"ymap"` will return `doc.get(key, Y.Map)`. |
+
+
+---
+
+#### `send_yjs_binary_update`
+
+This endpoint is used to send a Yjs binary update to the room’s Yjs document. You can use this endpoint to initialize Yjs data for the room or to update the room’s Yjs document. To send an update to a subdocument instead of the main document, pass its `guid`. Corresponds to [`liveblocks.sendYjsBinaryUpdate`](https://liveblocks.io/docs/api-reference/liveblocks-node#put-rooms-roomId-ydoc).
+
+The update is typically obtained by calling `Y.encodeStateAsUpdate(doc)`. See the [Yjs documentation](https://docs.yjs.dev/api/document-updates) for more details. When manually making this HTTP call, set the HTTP header `Content-Type` to `application/octet-stream`, and send the binary update (a `Uint8Array`) in the body of the HTTP request. This endpoint does not accept JSON, unlike most other endpoints.
+
+**Example**
+```python
+client.send_yjs_binary_update(
+ room_id="my-room-id",
+ body=...,
+ # guid="subdoc-guid-123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `guid` | `str \| Unset` | No | ID of the subdocument |
+| `body` | `File` | Yes | Request body (application/octet-stream) |
+
+
+---
+
+#### `get_yjs_document_as_binary_update`
+
+This endpoint returns the room's Yjs document encoded as a single binary update. This can be used by `Y.applyUpdate(responseBody)` to get a copy of the document in your back end. See [Yjs documentation](https://docs.yjs.dev/api/document-updates) for more information on working with updates. To return a subdocument instead of the main document, pass its `guid`. Corresponds to [`liveblocks.getYjsDocumentAsBinaryUpdate`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc-binary).
+
+**Example**
+```python
+result = client.get_yjs_document_as_binary_update(
+ room_id="my-room-id",
+ # guid="subdoc-guid-123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `guid` | `str \| Unset` | No | ID of the subdocument |
+
+
+---
+
+#### `get_yjs_versions`
+
+This endpoint returns a list of version history snapshots for the room's Yjs document. The versions are returned sorted by creation date, from newest to oldest.
+
+**Example**
+```python
+result = client.get_yjs_versions(
+ room_id="my-room-id",
+ # limit=20,
+ # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `limit` | `int \| Unset` | No | A limit on the number of versions to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)* |
+| `cursor` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+
+
+---
+
+#### `get_yjs_version`
+
+This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update.
+
+**Example**
+```python
+result = client.get_yjs_version(
+ room_id="my-room-id",
+ version_id="vh_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `version_id` | `str` | Yes | ID of the version |
+
+
+---
+
+#### `create_yjs_version`
+
+This endpoint creates a new version history snapshot for the room's Yjs document.
+
+**Example**
+```python
+result = client.create_yjs_version(
+ room_id="my-room-id",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+
+
+---
+
+### Comments
+
+#### `get_threads`
+
+This endpoint returns the threads in the requested room. Corresponds to [`liveblocks.getThreads`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads).
+
+**Example**
+```python
+result = client.get_threads(
+ room_id="my-room-id",
+ # query="metadata[\"color\"]:\"blue\"",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `query` | `str \| Unset` | No | Query to filter threads. You can filter by `metadata` and `resolved`, for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND resolved:true`. Learn more about [filtering threads with query language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language). |
+
+
+---
+
+#### `create_thread`
+
+This endpoint creates a new thread and the first comment in the thread. Corresponds to [`liveblocks.createThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads).
+
+A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `comment.body`.
+
+```json
+{
+ "version": 1,
+ "content": [
+ {
+ "type": "paragraph",
+ "children": [{ "text": "Hello " }, { "text": "world", "bold": true }]
+ }
+ ]
+}
+```
+
+`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+
+**Example**
+```python
+from liveblocks.models import CreateThreadRequestBody
+
+result = client.create_thread(
+ room_id="my-room-id",
+ body=CreateThreadRequestBody(
+ comment=...,
+ # metadata=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `body` | `CreateThreadRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `get_thread`
+
+This endpoint returns a thread by its ID. Corresponds to [`liveblocks.getThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId).
+
+**Example**
+```python
+result = client.get_thread(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+
+
+---
+
+#### `delete_thread`
+
+This endpoint deletes a thread by its ID. Corresponds to [`liveblocks.deleteThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-threads-threadId).
+
+**Example**
+```python
+client.delete_thread(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+
+
+---
+
+#### `get_thread_participants`
+
+**Deprecated.** Prefer using [thread subscriptions](#get-rooms-roomId-threads-threadId-subscriptions) instead.
+
+This endpoint returns the list of thread participants. It is a list of unique user IDs representing all the thread comment authors and mentioned users in comments. Corresponds to [`liveblocks.getThreadParticipants`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-participants).
+
+**Example**
+```python
+result = client.get_thread_participants(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+
+
+---
+
+#### `edit_thread_metadata`
+
+This endpoint edits the metadata of a thread. The metadata is a JSON object that can be used to store any information you want about the thread, in `string`, `number`, or `boolean` form. Set a property to `null` to remove it. Corresponds to [`liveblocks.editThreadMetadata`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-metadata).
+
+`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+
+**Example**
+```python
+from liveblocks.models import EditThreadMetadataRequestBody
+
+result = client.edit_thread_metadata(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=EditThreadMetadataRequestBody(
+ metadata=...,
+ user_id="...",
+ # updated_at=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `body` | `EditThreadMetadataRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `mark_thread_as_resolved`
+
+This endpoint marks a thread as resolved. The request body must include a `userId` to identify who resolved the thread. Returns the updated thread. Corresponds to [`liveblocks.markThreadAsResolved`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-mark-as-resolved).
+
+**Example**
+```python
+from liveblocks.models import MarkThreadAsResolvedRequestBody
+
+result = client.mark_thread_as_resolved(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=MarkThreadAsResolvedRequestBody(
+ user_id="...",
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `body` | `MarkThreadAsResolvedRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `mark_thread_as_unresolved`
+
+This endpoint marks a thread as unresolved. The request body must include a `userId` to identify who unresolved the thread. Returns the updated thread. Corresponds to [`liveblocks.markThreadAsUnresolved`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-mark-as-unresolved).
+
+**Example**
+```python
+from liveblocks.models import MarkThreadAsUnresolvedRequestBody
+
+result = client.mark_thread_as_unresolved(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=MarkThreadAsUnresolvedRequestBody(
+ user_id="...",
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `body` | `MarkThreadAsUnresolvedRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `subscribe_to_thread`
+
+This endpoint subscribes to a thread. Corresponds to [`liveblocks.subscribeToThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-subscribe).
+
+**Example**
+```python
+from liveblocks.models import SubscribeToThreadRequestBody
+
+result = client.subscribe_to_thread(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=SubscribeToThreadRequestBody(
+ user_id="...",
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `body` | `SubscribeToThreadRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `unsubscribe_from_thread`
+
+This endpoint unsubscribes from a thread. Corresponds to [`liveblocks.unsubscribeFromThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-unsubscribe).
+
+**Example**
+```python
+from liveblocks.models import UnsubscribeFromThreadRequestBody
+
+client.unsubscribe_from_thread(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=UnsubscribeFromThreadRequestBody(
+ user_id="...",
+ ),
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `body` | `UnsubscribeFromThreadRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `get_thread_subscriptions`
+
+This endpoint gets the list of subscriptions to a thread. Corresponds to [`liveblocks.getThreadSubscriptions`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-subscriptions).
+
+**Example**
+```python
+result = client.get_thread_subscriptions(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+
+
+---
+
+#### `create_comment`
+
+This endpoint creates a new comment, adding it as a reply to a thread. Corresponds to [`liveblocks.createComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments).
+
+A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `body`.
+
+```json
+{
+ "version": 1,
+ "content": [
+ {
+ "type": "paragraph",
+ "children": [{ "text": "Hello " }, { "text": "world", "bold": true }]
+ }
+ ]
+}
+```
+
+`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+
+**Example**
+```python
+from liveblocks.models import CreateCommentRequestBody
+
+result = client.create_comment(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=CreateCommentRequestBody(
+ user_id="...",
+ body=...,
+ # created_at=...,
+ # metadata=...,
+ # attachment_ids=[],
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `body` | `CreateCommentRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `get_comment`
+
+This endpoint returns a comment by its ID. Corresponds to [`liveblocks.getComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-comments-commentId).
+
+**Example**
+```python
+result = client.get_comment(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `comment_id` | `str` | Yes | ID of the comment |
+
+
+---
+
+#### `edit_comment`
+
+This endpoint edits the specified comment. Corresponds to [`liveblocks.editComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId).
+
+A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `body`.
+
+```json
+{
+ "version": 1,
+ "content": [
+ {
+ "type": "paragraph",
+ "children": [{ "text": "Hello " }, { "text": "world", "bold": true }]
+ }
+ ]
+}
+```
+
+**Example**
+```python
+from liveblocks.models import EditCommentRequestBody
+
+result = client.edit_comment(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+ body=EditCommentRequestBody(
+ body=...,
+ # edited_at=...,
+ # metadata=...,
+ # attachment_ids=[],
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `comment_id` | `str` | Yes | ID of the comment |
+| `body` | `EditCommentRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `delete_comment`
+
+This endpoint deletes a comment. A deleted comment is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.deleteComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId).
+
+**Example**
+```python
+client.delete_comment(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `comment_id` | `str` | Yes | ID of the comment |
+
+
+---
+
+#### `add_comment_reaction`
+
+This endpoint adds a reaction to a comment. Corresponds to [`liveblocks.addCommentReaction`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction).
+
+**Example**
+```python
+from liveblocks.models import AddCommentReactionRequestBody
+
+result = client.add_comment_reaction(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+ body=AddCommentReactionRequestBody(
+ user_id="...",
+ emoji="...",
+ # created_at=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `comment_id` | `str` | Yes | ID of the comment |
+| `body` | `AddCommentReactionRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `remove_comment_reaction`
+
+This endpoint removes a comment reaction. A deleted comment reaction is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.removeCommentReaction`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction).
+
+**Example**
+```python
+client.remove_comment_reaction(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `comment_id` | `str` | Yes | ID of the comment |
+| `body` | `RemoveCommentReactionRequestBody \| Unset` | No | Request body (application/json) |
+
+
+---
+
+#### `edit_comment_metadata`
+
+This endpoint edits the metadata of a comment. The metadata is a JSON object that can be used to store any information you want about the comment, in `string`, `number`, or `boolean` form. Set a property to `null` to remove it. Corresponds to [`liveblocks.editCommentMetadata`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-metadata).
+
+`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+
+**Example**
+```python
+from liveblocks.models import EditCommentMetadataRequestBody
+
+result = client.edit_comment_metadata(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+ body=EditCommentMetadataRequestBody(
+ metadata=...,
+ user_id="...",
+ # updated_at=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `thread_id` | `str` | Yes | ID of the thread |
+| `comment_id` | `str` | Yes | ID of the comment |
+| `body` | `EditCommentMetadataRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+### Auth
+
+#### `authorize_user`
+
+This endpoint lets your application server (your back end) obtain a token that one of its clients (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own application’s custom authentication endpoint. When making this request, you’ll have to use your secret key.
+
+**Important:** The difference with an [ID token](#post-identify-user) is that an access token holds all the permissions, and is the source of truth. With ID tokens, permissions are set in the Liveblocks back end (through REST API calls) and "checked at the door" every time they are used to enter a room.
+
+**Note:** When using the `@liveblocks/node` package, you can use [`Liveblocks.prepareSession`](https://liveblocks.io/docs/api-reference/liveblocks-node#access-tokens) in your back end to build this request.
+
+You can pass the property `userId` in the request’s body. This can be whatever internal identifier you use for your user accounts as long as it uniquely identifies an account. The property `userId` is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId` corresponds to one MAU.
+
+Additionally, you can set custom metadata to the token, which will be publicly accessible by other clients through the `user.info` property. This is useful for storing static data like avatar images or the user’s display name.
+
+Lastly, you’ll specify the exact permissions to give to the user using the `permissions` field. This is done in an object where the keys are room names, or room name patterns (ending in a `*`), and a list of permissions to assign the user for any room that matches that name exactly (or starts with the pattern’s prefix). For tips, see [Manage permissions with access tokens](https://liveblocks.io/docs/authentication/access-token).
+
+**Example**
+```python
+from liveblocks.models import AuthorizeUserRequestBody
+
+result = client.authorize_user(
+ body=AuthorizeUserRequestBody(
+ user_id="...",
+ permissions=...,
+ # user_info=...,
+ # organization_id="...",
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `body` | `AuthorizeUserRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `identify_user`
+
+This endpoint lets your application server (your back end) obtain a token that one of its clients (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own application’s custom authentication endpoint. When using this endpoint to obtain ID tokens, you should manage your permissions by assigning user and/or group permissions to rooms explicitly, see our [Manage permissions with ID tokens](https://liveblocks.io/docs/authentication/id-token) section.
+
+**Important:** The difference with an [access token](#post-authorize-user) is that an ID token doesn’t hold any permissions itself. With ID tokens, permissions are set in the Liveblocks back end (through REST API calls) and "checked at the door" every time they are used to enter a room. With access tokens, all permissions are set in the token itself, and thus controlled from your back end entirely.
+
+**Note:** When using the `@liveblocks/node` package, you can use [`Liveblocks.identifyUser`](https://liveblocks.io/docs/api-reference/liveblocks-node) in your back end to build this request.
+
+You can pass the property `userId` in the request’s body. This can be whatever internal identifier you use for your user accounts as long as it uniquely identifies an account. The property `userId` is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId` corresponds to one MAU.
+
+If you want to use group permissions, you can also declare which `groupIds` this user belongs to. The group ID values are yours, but they will have to match the group IDs you assign permissions to when assigning permissions to rooms, see [Manage permissions with ID tokens](https://liveblocks.io/docs/authentication/id-token)).
+
+Additionally, you can set custom metadata to the token, which will be publicly accessible by other clients through the `user.info` property. This is useful for storing static data like avatar images or the user’s display name.
+
+**Example**
+```python
+from liveblocks.models import IdentifyUserRequestBody
+
+result = client.identify_user(
+ body=IdentifyUserRequestBody(
+ user_id="...",
+ # organization_id="...",
+ # group_ids=[],
+ # user_info=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `body` | `IdentifyUserRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+### Notifications
+
+#### `get_inbox_notification`
+
+This endpoint returns a user’s inbox notification by its ID. Corresponds to [`liveblocks.getInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications-inboxNotificationId).
+
+**Example**
+```python
+result = client.get_inbox_notification(
+ user_id="user-123",
+ inbox_notification_id="in_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `user_id` | `str` | Yes | ID of the user |
+| `inbox_notification_id` | `str` | Yes | ID of the inbox notification |
+
+
+---
+
+#### `delete_inbox_notification`
+
+This endpoint deletes a user’s inbox notification by its ID. Corresponds to [`liveblocks.deleteInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-users-userId-inbox-notifications-inboxNotificationId).
+
+**Example**
+```python
+client.delete_inbox_notification(
+ user_id="user-123",
+ inbox_notification_id="in_abc123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `user_id` | `str` | Yes | ID of the user |
+| `inbox_notification_id` | `str` | Yes | ID of the inbox notification |
+
+
+---
+
+#### `get_inbox_notifications`
+
+This endpoint returns all the user’s inbox notifications. Corresponds to [`liveblocks.getInboxNotifications`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications).
+
+**Example**
+```python
+result = client.get_inbox_notifications(
+ user_id="user-123",
+ # organization_id="org_123456789",
+ # query="metadata[\"color\"]:\"blue\"",
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `user_id` | `str` | Yes | ID of the user |
+| `organization_id` | `str \| Unset` | No | The organization ID to filter notifications for. |
+| `query` | `str \| Unset` | No | Query to filter notifications. You can filter by `unread`, for example, `unread:true`. |
+| `limit` | `int \| Unset` | No | A limit on the number of inbox notifications to be returned. The limit can range between 1 and 50, and defaults to 50. *(default: `50`)* |
+| `starting_after` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+
+
+---
+
+#### `delete_all_inbox_notifications`
+
+This endpoint deletes all the user’s inbox notifications. Corresponds to [`liveblocks.deleteAllInboxNotifications`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-users-userId-inbox-notifications).
+
+**Example**
+```python
+client.delete_all_inbox_notifications(
+ user_id="user-123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `user_id` | `str` | Yes | ID of the user |
+
+
+---
+
+#### `get_notification_settings`
+
+This endpoint returns a user's notification settings for the project. Corresponds to [`liveblocks.getNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-notification-settings).
+
+**Example**
+```python
+result = client.get_notification_settings(
+ user_id="user-123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `user_id` | `str` | Yes | ID of the user |
+
+
+---
+
+#### `update_notification_settings`
+
+This endpoint updates a user's notification settings for the project. Corresponds to [`liveblocks.updateNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-users-userId-notification-settings).
+
+**Example**
+```python
+from liveblocks.models import UpdateNotificationSettingsRequestBody
+
+result = client.update_notification_settings(
+ user_id="user-123",
+ body=UpdateNotificationSettingsRequestBody(
+ # email=...,
+ # slack=...,
+ # teams=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `user_id` | `str` | Yes | ID of the user |
+| `body` | `UpdateNotificationSettingsRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `delete_notification_settings`
+
+This endpoint deletes a user's notification settings for the project. Corresponds to [`liveblocks.deleteNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-users-userId-notification-settings).
+
+**Example**
+```python
+client.delete_notification_settings(
+ user_id="user-123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `user_id` | `str` | Yes | ID of the user |
+
+
+---
+
+#### `get_room_subscription_settings`
+
+This endpoint returns a user’s subscription settings for a specific room. Corresponds to [`liveblocks.getRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-users-userId-subscription-settings).
+
+**Example**
+```python
+result = client.get_room_subscription_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `user_id` | `str` | Yes | ID of the user |
+
+
+---
+
+#### `update_room_subscription_settings`
+
+This endpoint updates a user’s subscription settings for a specific room. Corresponds to [`liveblocks.updateRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-users-userId-subscription-settings).
+
+**Example**
+```python
+from liveblocks.models import UpdateRoomSubscriptionSettingsRequestBody
+
+result = client.update_room_subscription_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+ body=UpdateRoomSubscriptionSettingsRequestBody(
+ # threads=...,
+ # text_mentions=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `user_id` | `str` | Yes | ID of the user |
+| `body` | `UpdateRoomSubscriptionSettingsRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `delete_room_subscription_settings`
+
+This endpoint deletes a user’s subscription settings for a specific room. Corresponds to [`liveblocks.deleteRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-users-userId-subscription-settings).
+
+**Example**
+```python
+client.delete_room_subscription_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `user_id` | `str` | Yes | ID of the user |
+
+
+---
+
+#### `get_user_room_subscription_settings`
+
+This endpoint returns the list of a user's room subscription settings. Corresponds to [`liveblocks.getUserRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-room-subscription-settings).
+
+**Example**
+```python
+result = client.get_user_room_subscription_settings(
+ user_id="user-123",
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+ # limit=20,
+ # organization_id="org_123456789",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `user_id` | `str` | Yes | ID of the user |
+| `starting_after` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+| `limit` | `int \| Unset` | No | A limit on the number of elements to be returned. The limit can range between 1 and 50, and defaults to 50. *(default: `50`)* |
+| `organization_id` | `str \| Unset` | No | The organization ID to filter room subscription settings for. |
+
+
+---
+
+#### `get_room_notification_settings`
+
+**Deprecated.** Renamed to [`/subscription-settings`](get-room-subscription-settings). Read more in our [migration guide](https://liveblocks.io/docs/platform/upgrading/2.24).
+
+This endpoint returns a user’s subscription settings for a specific room. Corresponds to [`liveblocks.getRoomNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-users-userId-notification-settings).
+
+**Example**
+```python
+result = client.get_room_notification_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `user_id` | `str` | Yes | ID of the user |
+
+
+---
+
+#### `update_room_notification_settings`
+
+**Deprecated.** Renamed to [`/subscription-settings`](update-room-subscription-settings). Read more in our [migration guide](https://liveblocks.io/docs/platform/upgrading/2.24).
+
+This endpoint updates a user’s notification settings for a specific room. Corresponds to [`liveblocks.updateRoomNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-users-userId-notification-settings).
+
+**Example**
+```python
+result = client.update_room_notification_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `user_id` | `str` | Yes | ID of the user |
+| `body` | `UpdateRoomSubscriptionSettingsRequestBody \| Unset` | No | Request body (application/json) |
+
+
+---
+
+#### `delete_room_notification_settings`
+
+**Deprecated.** Renamed to [`/subscription-settings`](delete-room-subscription-settings). Read more in our [migration guide](https://liveblocks.io/docs/platform/upgrading/2.24).
+
+This endpoint deletes a user’s notification settings for a specific room. Corresponds to [`liveblocks.deleteRoomNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-users-userId-notification-settings).
+
+**Example**
+```python
+client.delete_room_notification_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `user_id` | `str` | Yes | ID of the user |
+
+
+---
+
+#### `trigger_inbox_notification`
+
+This endpoint triggers an inbox notification. Corresponds to [`liveblocks.triggerInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger).
+
+**Example**
+```python
+client.trigger_inbox_notification()
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `body` | `TriggerInboxNotificationRequestBody \| Unset` | No | Request body (application/json) |
+
+
+---
+
+### Groups
+
+#### `get_groups`
+
+This endpoint returns a list of all groups in your project. Corresponds to [`liveblocks.getGroups`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-groups).
+
+**Example**
+```python
+result = client.get_groups(
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `limit` | `int \| Unset` | No | A limit on the number of groups to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)* |
+| `starting_after` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+
+
+---
+
+#### `create_group`
+
+This endpoint creates a new group. Corresponds to [`liveblocks.createGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-group).
+
+**Example**
+```python
+result = client.create_group()
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `body` | `CreateGroupRequestBody \| Unset` | No | Request body (application/json) |
+
+
+---
+
+#### `get_group`
+
+This endpoint returns a specific group by ID. Corresponds to [`liveblocks.getGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-group).
+
+**Example**
+```python
+result = client.get_group(
+ group_id="engineering",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `group_id` | `str` | Yes | The ID of the group to retrieve. |
+
+
+---
+
+#### `delete_group`
+
+This endpoint deletes a group. Corresponds to [`liveblocks.deleteGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-group).
+
+**Example**
+```python
+client.delete_group(
+ group_id="engineering",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `group_id` | `str` | Yes | The ID of the group to delete. |
+
+
+---
+
+#### `add_group_members`
+
+This endpoint adds new members to an existing group. Corresponds to [`liveblocks.addGroupMembers`](https://liveblocks.io/docs/api-reference/liveblocks-node#add-group-members).
+
+**Example**
+```python
+from liveblocks.models import AddGroupMembersRequestBody
+
+result = client.add_group_members(
+ group_id="engineering",
+ body=AddGroupMembersRequestBody(
+ member_ids=[],
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `group_id` | `str` | Yes | The ID of the group to add members to. |
+| `body` | `AddGroupMembersRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `remove_group_members`
+
+This endpoint removes members from an existing group. Corresponds to [`liveblocks.removeGroupMembers`](https://liveblocks.io/docs/api-reference/liveblocks-node#remove-group-members).
+
+**Example**
+```python
+from liveblocks.models import RemoveGroupMembersRequestBody
+
+result = client.remove_group_members(
+ group_id="engineering",
+ body=RemoveGroupMembersRequestBody(
+ member_ids=[],
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `group_id` | `str` | Yes | The ID of the group to remove members from. |
+| `body` | `RemoveGroupMembersRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `get_user_groups`
+
+This endpoint returns all groups that a specific user is a member of. Corresponds to [`liveblocks.getUserGroups`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-user-groups).
+
+**Example**
+```python
+result = client.get_user_groups(
+ user_id="user-123",
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `user_id` | `str` | Yes | The ID of the user to get groups for. |
+| `limit` | `int \| Unset` | No | A limit on the number of groups to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)* |
+| `starting_after` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+
+
+---
+
+### Ai
+
+#### `get_ai_copilots`
+
+This endpoint returns a paginated list of AI copilots. The copilots are returned sorted by creation date, from newest to oldest. Corresponds to [`liveblocks.getAiCopilots`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-ai-copilots).
+
+**Example**
+```python
+result = client.get_ai_copilots(
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `limit` | `int \| Unset` | No | A limit on the number of copilots to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)* |
+| `starting_after` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+
+
+---
+
+#### `create_ai_copilot`
+
+This endpoint creates a new AI copilot with the given configuration. Corresponds to [`liveblocks.createAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-ai-copilot).
+
+**Example**
+```python
+result = client.create_ai_copilot(
+ body=...,
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `body` | `CreateAiCopilotOptionsAnthropic \| CreateAiCopilotOptionsGoogle \| CreateAiCopilotOptionsOpenAi \| CreateAiCopilotOptionsOpenAiCompatible` | Yes | Request body (application/json) |
+
+
+---
+
+#### `get_ai_copilot`
+
+This endpoint returns an AI copilot by its ID. Corresponds to [`liveblocks.getAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-ai-copilot).
+
+**Example**
+```python
+result = client.get_ai_copilot(
+ copilot_id="cp_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `copilot_id` | `str` | Yes | ID of the AI copilot |
+
+
+---
+
+#### `update_ai_copilot`
+
+This endpoint updates an existing AI copilot's configuration. Corresponds to [`liveblocks.updateAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#update-ai-copilot).
+
+This endpoint returns a 422 response if the update doesn't apply due to validation failures. For example, if the existing copilot uses the "openai" provider and you attempt to update the provider model to an incompatible value for the provider, like "gemini-2.5-pro", you'll receive a 422 response with an error message explaining where the validation failed.
+
+**Example**
+```python
+from liveblocks.models import UpdateAiCopilotRequestBody
+
+result = client.update_ai_copilot(
+ copilot_id="cp_abc123",
+ body=UpdateAiCopilotRequestBody(
+ # name="...",
+ # description="...",
+ # system_prompt="...",
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `copilot_id` | `str` | Yes | ID of the AI copilot |
+| `body` | `UpdateAiCopilotRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `delete_ai_copilot`
+
+This endpoint deletes an AI copilot by its ID. A deleted copilot is no longer accessible and cannot be restored. Corresponds to [`liveblocks.deleteAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-ai-copilot).
+
+**Example**
+```python
+client.delete_ai_copilot(
+ copilot_id="cp_abc123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `copilot_id` | `str` | Yes | ID of the AI copilot |
+
+
+---
+
+#### `get_knowledge_sources`
+
+This endpoint returns a paginated list of knowledge sources for a specific AI copilot. Corresponds to [`liveblocks.getKnowledgeSources`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-knowledge-sources).
+
+**Example**
+```python
+result = client.get_knowledge_sources(
+ copilot_id="cp_abc123",
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `copilot_id` | `str` | Yes | ID of the AI copilot |
+| `limit` | `int \| Unset` | No | A limit on the number of knowledge sources to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)* |
+| `starting_after` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+
+
+---
+
+#### `get_knowledge_source`
+
+This endpoint returns a specific knowledge source by its ID. Corresponds to [`liveblocks.getKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-knowledge-source).
+
+**Example**
+```python
+result = client.get_knowledge_source(
+ copilot_id="cp_abc123",
+ knowledge_source_id="ks_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `copilot_id` | `str` | Yes | ID of the AI copilot |
+| `knowledge_source_id` | `str` | Yes | ID of the knowledge source |
+
+
+---
+
+#### `create_web_knowledge_source`
+
+This endpoint creates a web knowledge source for an AI copilot. This allows the copilot to access and learn from web content. Corresponds to [`liveblocks.createWebKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-web-knowledge-source).
+
+**Example**
+```python
+from liveblocks.models import CreateWebKnowledgeSourceRequestBody
+
+result = client.create_web_knowledge_source(
+ copilot_id="cp_abc123",
+ body=CreateWebKnowledgeSourceRequestBody(
+ copilot_id="...",
+ url="...",
+ type_=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `copilot_id` | `str` | Yes | ID of the AI copilot |
+| `body` | `CreateWebKnowledgeSourceRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `create_file_knowledge_source`
+
+This endpoint creates a file knowledge source for an AI copilot by uploading a file. The copilot can then reference the content of the file when responding. Corresponds to [`liveblocks.createFileKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-file-knowledge-source).
+
+**Example**
+```python
+result = client.create_file_knowledge_source(
+ copilot_id="cp_abc123",
+ name="document.pdf",
+ body=...,
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `copilot_id` | `str` | Yes | ID of the AI copilot |
+| `name` | `str` | Yes | Name of the file |
+| `body` | `File` | Yes | Request body (application/octet-stream) |
+
+
+---
+
+#### `get_file_knowledge_source_markdown`
+
+This endpoint returns the content of a file knowledge source as markdown. This allows you to see what content the AI copilot has access to from uploaded files. Corresponds to [`liveblocks.getFileKnowledgeSourceMarkdown`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-file-knowledge-source-markdown).
+
+**Example**
+```python
+result = client.get_file_knowledge_source_markdown(
+ copilot_id="cp_abc123",
+ knowledge_source_id="ks_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `copilot_id` | `str` | Yes | ID of the AI copilot |
+| `knowledge_source_id` | `str` | Yes | ID of the knowledge source |
+
+
+---
+
+#### `delete_file_knowledge_source`
+
+This endpoint deletes a file knowledge source from an AI copilot. The copilot will no longer have access to the content from this file. Corresponds to [`liveblocks.deleteFileKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-file-knowledge-source).
+
+**Example**
+```python
+client.delete_file_knowledge_source(
+ copilot_id="cp_abc123",
+ knowledge_source_id="ks_abc123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `copilot_id` | `str` | Yes | ID of the AI copilot |
+| `knowledge_source_id` | `str` | Yes | ID of the knowledge source |
+
+
+---
+
+#### `delete_web_knowledge_source`
+
+This endpoint deletes a web knowledge source from an AI copilot. The copilot will no longer have access to the content from this source. Corresponds to [`liveblocks.deleteWebKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-web-knowledge-source).
+
+**Example**
+```python
+client.delete_web_knowledge_source(
+ copilot_id="cp_abc123",
+ knowledge_source_id="ks_abc123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `copilot_id` | `str` | Yes | ID of the AI copilot |
+| `knowledge_source_id` | `str` | Yes | ID of the knowledge source |
+
+
+---
+
+#### `get_web_knowledge_source_links`
+
+This endpoint returns a paginated list of links that were indexed from a web knowledge source. This is useful for understanding what content the AI copilot has access to from web sources. Corresponds to [`liveblocks.getWebKnowledgeSourceLinks`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-web-knowledge-source-links).
+
+**Example**
+```python
+result = client.get_web_knowledge_source_links(
+ copilot_id="cp_abc123",
+ knowledge_source_id="ks_abc123",
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `copilot_id` | `str` | Yes | ID of the AI copilot |
+| `knowledge_source_id` | `str` | Yes | ID of the knowledge source |
+| `limit` | `int \| Unset` | No | A limit on the number of links to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)* |
+| `starting_after` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+
+
+---
+
+### Management
+
+#### `get_management_projects`
+
+Returns a paginated list of projects. You can limit the number of projects returned per page and use the provided `nextCursor` for pagination. This endpoint requires the `read:all` scope.
+
+**Example**
+```python
+result = client.get_management_projects(
+ # limit=20,
+ # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `limit` | `int \| Unset` | No | A limit on the number of projects to return. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)* |
+| `cursor` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+
+
+---
+
+#### `create_management_project`
+
+Creates a new project within your account. This endpoint requires the `write:all` scope. You can specify the project type, name, and version creation timeout. Upon success, returns information about the newly created project, including its ID, keys, region, and settings.
+
+**Example**
+```python
+from liveblocks.models import CreateManagementProjectRequestBody
+
+result = client.create_management_project(
+ body=CreateManagementProjectRequestBody(
+ type_=...,
+ # name="...",
+ # version_creation_timeout=False,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `body` | `CreateManagementProjectRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `get_management_project`
+
+Returns a single project specified by its ID. This endpoint requires the `read:all` scope. If the project cannot be found, a 404 error response is returned.
+
+**Example**
+```python
+result = client.get_management_project(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+
+
+---
+
+#### `update_management_project`
+
+Updates an existing project specified by its ID. This endpoint allows you to modify project details such as the project name and the version creation timeout. The `versionCreationTimeout` can be set to `false` to disable the timeout or to a number of seconds between 30 and 300. Fields omitted from the request body will not be updated. Requires the `write:all` scope.
+
+If the project cannot be found, a 404 error response is returned.
+
+**Example**
+```python
+from liveblocks.models import UpdateManagementProjectRequestBody
+
+result = client.update_management_project(
+ project_id="683d49ed6b4d1cec5a597b13",
+ body=UpdateManagementProjectRequestBody(
+ # name="...",
+ # version_creation_timeout=False,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `body` | `UpdateManagementProjectRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `delete_management_project`
+
+Soft deletes the project specified by its ID. This endpoint requires the `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+**Example**
+```python
+client.delete_management_project(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+
+
+---
+
+#### `activate_project_public_api_key`
+
+Activates the public API key associated with the specified project. This endpoint requires the `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+**Example**
+```python
+client.activate_project_public_api_key(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+
+
+---
+
+#### `deactivate_project_public_api_key`
+
+Deactivates the public API key associated with the specified project. This endpoint requires the `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+**Example**
+```python
+client.deactivate_project_public_api_key(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+
+
+---
+
+#### `roll_project_public_api_key`
+
+Rolls (rotates) the public API key associated with the specified project, generating a new key value while deprecating the previous one. The new key becomes immediately active. This endpoint requires the `write:all` scope.
+
+If the public key is not currently enabled for the project, a 403 error response is returned. If the project cannot be found, a 404 error response is returned. An optional `expirationIn` parameter can be provided in the request body to set when the previous key should expire.
+
+**Example**
+```python
+result = client.roll_project_public_api_key(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `body` | `RollProjectPublicApiKeyRequestBody \| Unset` | No | Request body (application/json) |
+
+
+---
+
+#### `roll_project_secret_api_key`
+
+Rolls (rotates) the secret API key associated with the specified project, generating a new key value while deprecating the previous one. The new key becomes immediately active. This endpoint requires the `write:all` scope.
+
+If the project cannot be found, a 404 error response is returned. An optional `expirationIn` parameter can be provided in the request body to set when the previous key should expire.
+
+**Example**
+```python
+result = client.roll_project_secret_api_key(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `body` | `RollProjectSecretApiKeyRequestBody \| Unset` | No | Request body (application/json) |
+
+
+---
+
+#### `get_management_webhooks`
+
+Returns a paginated list of webhooks for a project. This endpoint requires the `read:all` scope. The response includes an array of webhook objects associated with the specified project, as well as a `nextCursor` property for pagination. Use the `limit` query parameter to specify the maximum number of webhooks to return (1-100, default 20). If the result is paginated, use the `cursor` parameter from the `nextCursor` value in the previous response to fetch subsequent pages. If the project cannot be found, a 404 error response is returned.
+
+**Example**
+```python
+result = client.get_management_webhooks(
+ project_id="683d49ed6b4d1cec5a597b13",
+ # limit=20,
+ # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `limit` | `int \| Unset` | No | A limit on the number of webhooks to return. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)* |
+| `cursor` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+
+
+---
+
+#### `create_management_webhook`
+
+Creates a new webhook for a project. This endpoint requires the `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+**Example**
+```python
+from liveblocks.models import CreateManagementWebhookRequestBody
+
+result = client.create_management_webhook(
+ project_id="683d49ed6b4d1cec5a597b13",
+ body=CreateManagementWebhookRequestBody(
+ url="...",
+ subscribed_events=[],
+ # rate_limit=0,
+ # additional_headers=...,
+ # storage_updated_throttle_seconds=0,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `body` | `CreateManagementWebhookRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `get_management_webhook`
+
+Get one webhook by `webhookId` for a project. Returns webhook settings such as URL, subscribed events, disabled state, throttling, and additional headers. Returns `404` if the project or webhook does not exist. This endpoint requires the `read:all` scope.
+
+**Example**
+```python
+result = client.get_management_webhook(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `webhook_id` | `str` | Yes | ID of the webhook |
+
+
+---
+
+#### `update_management_webhook`
+
+Update one webhook by `webhookId` for a project. Send only fields you want to change; omitted fields stay unchanged. Returns `404` if the project or webhook does not exist and `422` for validation errors. This endpoint requires the `write:all` scope.
+
+**Example**
+```python
+from liveblocks.models import UpdateManagementWebhookRequestBody
+
+result = client.update_management_webhook(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+ body=UpdateManagementWebhookRequestBody(
+ # url="...",
+ # subscribed_events=[],
+ # rate_limit=0,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `webhook_id` | `str` | Yes | ID of the webhook |
+| `body` | `UpdateManagementWebhookRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `delete_management_webhook`
+
+Delete one webhook by `webhookId` for a project. Returns `200` with an empty body on success, or `404` if the project or webhook does not exist. Requires `write:all`.
+
+**Example**
+```python
+client.delete_management_webhook(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `webhook_id` | `str` | Yes | ID of the webhook |
+
+
+---
+
+#### `roll_management_webhook_secret`
+
+Rotate a webhook signing secret and return the new secret. The previous secret remains valid for 24 hours. Returns `404` if the project or webhook does not exist. This endpoint requires the `write:all` scope.
+
+**Example**
+```python
+result = client.roll_management_webhook_secret(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `webhook_id` | `str` | Yes | ID of the webhook |
+
+
+---
+
+#### `get_management_webhook_additional_headers`
+
+Get a webhook's additional headers. Returns `404` if the project or webhook does not exist. Requires `read:all`.
+
+**Example**
+```python
+result = client.get_management_webhook_additional_headers(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `webhook_id` | `str` | Yes | ID of the webhook |
+
+
+---
+
+#### `upsert_management_webhook_additional_headers`
+
+Upsert additional headers for a webhook. Provided headers are merged with existing headers, and existing values are overwritten when names match. Returns updated headers, or `404` if the project or webhook does not exist. This endpoint requires the `write:all` scope.
+
+**Example**
+```python
+from liveblocks.models import UpsertManagementWebhookHeadersRequestBody
+
+result = client.upsert_management_webhook_additional_headers(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+ body=UpsertManagementWebhookHeadersRequestBody(
+ headers=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `webhook_id` | `str` | Yes | ID of the webhook |
+| `body` | `UpsertManagementWebhookHeadersRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `delete_management_webhook_additional_headers`
+
+Remove selected additional headers from a webhook. Send header names in `headers` field; other headers are unchanged. Returns updated headers, or `404` if the project or webhook does not exist. This endpoint requires the `write:all` scope. At least one header name must be provided; otherwise, a 422 error response is returned.
+
+**Example**
+```python
+from liveblocks.models import DeleteManagementWebhookHeadersRequestBody
+
+result = client.delete_management_webhook_additional_headers(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+ body=DeleteManagementWebhookHeadersRequestBody(
+ headers=[],
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `webhook_id` | `str` | Yes | ID of the webhook |
+| `body` | `DeleteManagementWebhookHeadersRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `recover_failed_webhook_messages`
+
+Requeue failed deliveries for a webhook from the given `since` timestamp. Returns `200` with an empty body when recovery starts, an `404` if the project or webhook does not exist. This endpoint requires the `write:all` scope.
+
+**Example**
+```python
+from liveblocks.models import RecoverManagementWebhookFailedMessagesRequestBody
+
+client.recover_failed_webhook_messages(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+ body=RecoverManagementWebhookFailedMessagesRequestBody(
+ since=...,
+ ),
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `webhook_id` | `str` | Yes | ID of the webhook |
+| `body` | `RecoverManagementWebhookFailedMessagesRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `send_test_webhook`
+
+Send a test event to a webhook and return the created message metadata. `subscribedEvent` must be one of the webhook's subscribed events, otherwise the endpoint returns `422`. Returns `404` if the project or webhook does not exist. This endpoint requires the `write:all` scope.
+
+**Example**
+```python
+from liveblocks.models import TestManagementWebhookRequestBody
+
+result = client.send_test_webhook(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+ body=TestManagementWebhookRequestBody(
+ subscribed_event=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `project_id` | `str` | Yes | ID of the project |
+| `webhook_id` | `str` | Yes | ID of the webhook |
+| `body` | `TestManagementWebhookRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+
+## Error Handling
+
+All API methods raise `errors.LiveblocksError` when the server returns a non-2xx status code. You can catch and inspect these errors:
+
+```python
+from liveblocks import errors, Liveblocks
+
+client = Liveblocks(secret="sk_your_secret_key")
+
+with client:
+ try:
+ room = client.get_room(room_id="my-room")
+ except errors.LiveblocksError as e:
+ print(f"API error: {e}")
+```
+
+Methods also raise `httpx.TimeoutException` if the request exceeds the timeout.
\ No newline at end of file
diff --git a/packages/liveblocks-python/README.mdx b/packages/liveblocks-python/README.mdx
new file mode 100644
index 0000000000..26005f83a8
--- /dev/null
+++ b/packages/liveblocks-python/README.mdx
@@ -0,0 +1,3364 @@
+---
+meta:
+ title: "@liveblocks/python"
+ parentTitle: "API Reference"
+ description: "API Reference for the @liveblocks/python package"
+alwaysShowAllNavigationLevels: false
+---
+
+`@liveblocks/python` provides you with a Python client for accessing the Liveblocks API. This library is only intended for use in your Python back end.
+
+## Installation
+
+Install the Liveblocks package to get started.
+
+```bash
+pip install liveblocks
+```
+
+## Quickstart
+
+All API calls require a Liveblocks client configured with your secret key, found in the [Liveblocks Dashboard](https://liveblocks.io/dashboard/apikeys). Methods can be called synchronously or asynchronously.
+
+```python title="Synchronous"
+from liveblocks import Liveblocks
+
+client = Liveblocks(secret="{{SECRET_KEY}}")
+
+with client:
+ rooms = client.get_rooms()
+ print(rooms)
+```
+
+```python title="Asynchronous"
+from liveblocks import AsyncLiveblocks
+
+client = AsyncLiveblocks(secret="{{SECRET_KEY}}")
+
+async with client:
+ rooms = await client.get_rooms()
+ print(rooms)
+```
+
+## Room
+
+### get_rooms
+
+This endpoint returns a list of your rooms. The rooms are returned sorted by creation date, from newest to oldest. You can filter rooms by room ID prefixes, metadata, users accesses, and groups accesses. Corresponds to [`liveblocks.getRooms`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms).
+
+There is a pagination system where the cursor to the next page is returned in the response as `nextCursor`, which can be combined with `startingAfter`.
+You can also limit the number of rooms by query.
+
+Filtering by metadata works by giving key values like `metadata.color=red`. Of course you can combine multiple metadata clauses to refine the response like `metadata.color=red&metadata.type=text`. Notice here the operator AND is applied between each clauses.
+
+Filtering by groups or userId works by giving a list of groups like `groupIds=marketing,GZo7tQ,product` or/and a userId like `userId=user1`.
+Notice here the operator OR is applied between each `groupIds` and the `userId`.
+
+
+```python
+result = client.get_rooms(
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+ # organization_id="org_123456789",
+ # query="metadata[\"color\"]:\"blue\"",
+ # user_id="user-123",
+ # group_ids="group1,group2",
+)
+print(result)
+```
+
+
+
+ A limit on the number of rooms to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)*
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+ A filter on organization ID.
+
+
+ Query to filter rooms. You can filter by `roomId` and `metadata`, for example, `metadata["roomType"]:"whiteboard" AND roomId^"liveblocks:engineering"`. Learn more about [filtering rooms with query language](https://liveblocks.io/docs/guides/how-to-filter-rooms-using-query-language).
+
+
+ A filter on users accesses.
+
+
+ A filter on groups accesses. Multiple groups can be used.
+
+
+
+
+### create_room
+
+This endpoint creates a new room. `id` and `defaultAccesses` are required. When provided with a `?idempotent` query argument, will not return a 409 when the room already exists, but instead return the existing room as-is. Corresponds to [`liveblocks.createRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms), or to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-create-rooms-roomId) when `?idempotent` is provided.
+- `defaultAccesses` could be `[]` or `["room:write"]` (private or public).
+- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.
+- `usersAccesses` could be `[]` or `["room:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+- `groupsAccesses` are optional fields.
+
+
+```python
+from liveblocks.models import CreateRoomRequestBody
+
+result = client.create_room(
+ body=CreateRoomRequestBody(
+ id="...",
+ default_accesses=[],
+ # organization_id="...",
+ # users_accesses=...,
+ # groups_accesses=...,
+ ),
+ # idempotent=True,
+)
+print(result)
+```
+
+
+
+ When provided, will not return a 409 when the room already exists, but instead return the existing room as-is. Corresponds to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-create-rooms-roomId).
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_room
+
+This endpoint returns a room by its ID. Corresponds to [`liveblocks.getRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomid).
+
+```python
+result = client.get_room(
+ room_id="my-room-id",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+
+
+### update_room
+
+This endpoint updates specific properties of a room. Corresponds to [`liveblocks.updateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomid).
+
+It’s not necessary to provide the entire room’s information.
+Setting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users:
+``{
+ "usersAccesses": {
+ "john": null
+ }
+}``
+`defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.
+
+- `defaultAccesses` could be `[]` or `["room:write"]` (private or public).
+- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.
+- `usersAccesses` could be `[]` or `["room:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+- `groupsAccesses` could be `[]` or `["room:write"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field.
+
+```python
+from liveblocks.models import UpdateRoomRequestBody
+
+result = client.update_room(
+ room_id="my-room-id",
+ body=UpdateRoomRequestBody(
+ # default_accesses=[],
+ # users_accesses=...,
+ # groups_accesses=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### delete_room
+
+This endpoint deletes a room. A deleted room is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.deleteRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomid).
+
+```python
+client.delete_room(
+ room_id="my-room-id",
+)
+```
+
+
+
+ ID of the room
+
+
+
+
+
+### prewarm_room
+
+Speeds up connecting to a room for the next 10 seconds. Use this when you know a user will be connecting to a room with [`RoomProvider`](https://liveblocks.io/docs/api-reference/liveblocks-react#RoomProvider) or [`enterRoom`](https://liveblocks.io/docs/api-reference/liveblocks-client#Client.enterRoom) within 10 seconds, and the room will load quicker. Corresponds to [`liveblocks.prewarmRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomid-prewarm).
+
+```python
+client.prewarm_room(
+ room_id="my-room-id",
+)
+```
+
+
+
+ ID of the room
+
+
+
+
+
+### upsert_room
+
+This endpoint updates specific properties of a room. Corresponds to [`liveblocks.upsertRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#upsert-rooms-roomId).
+
+It’s not necessary to provide the entire room’s information.
+Setting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users:
+``{
+ "usersAccesses": {
+ "john": null
+ }
+}``
+`defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.
+
+- `defaultAccesses` could be `[]` or `["room:write"]` (private or public).
+- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.
+- `usersAccesses` could be `[]` or `["room:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+- `groupsAccesses` could be `[]` or `["room:write"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field.
+
+```python
+from liveblocks.models import UpsertRoomRequestBody
+
+result = client.upsert_room(
+ room_id="my-room-id",
+ body=UpsertRoomRequestBody(
+ update=...,
+ # create=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### update_room_id
+
+This endpoint permanently updates the room’s ID. All existing references to the old room ID will need to be updated. Returns the updated room. Corresponds to [`liveblocks.updateRoomId`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomid-update-room-id).
+
+```python
+result = client.update_room_id(
+ room_id="my-room-id",
+)
+print(result)
+```
+
+
+
+ The new ID for the room
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_active_users
+
+This endpoint returns a list of users currently present in the requested room. Corresponds to [`liveblocks.getActiveUsers`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomid-active-users).
+
+For optimal performance, we recommend calling this endpoint no more than once every 10 seconds.
+Duplicates can occur if a user is in the requested room with multiple browser tabs opened.
+
+```python
+result = client.get_active_users(
+ room_id="my-room-id",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+
+
+### set_presence
+
+This endpoint sets ephemeral presence for a user in a room without requiring a WebSocket connection. The presence data will automatically expire after the specified TTL (time-to-live). This is useful for scenarios like showing an AI agent's presence in a room. The presence will be broadcast to all connected users in the room. Corresponds to [`liveblocks.setPresence`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-presence).
+
+```python
+from liveblocks.models import SetPresenceRequestBody
+
+client.set_presence(
+ room_id="my-room-id",
+ body=SetPresenceRequestBody(
+ user_id="...",
+ data=...,
+ # user_info=...,
+ # ttl=0,
+ ),
+)
+```
+
+
+
+ ID of the room
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### broadcast_event
+
+This endpoint enables the broadcast of an event to a room without having to connect to it via the `client` from `@liveblocks/client`. It takes any valid JSON as a request body. The `connectionId` passed to event listeners is `-1` when using this API. Corresponds to [`liveblocks.broadcastEvent`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-broadcast-event).
+
+```python
+client.broadcast_event(
+ room_id="my-room-id",
+ body=...,
+)
+```
+
+
+
+ ID of the room
+
+
+
+ Request body (application/json).
+
+
+
+
+
+## Storage
+
+### get_storage_document
+
+Returns the contents of the room’s Storage tree. Corresponds to [`liveblocks.getStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-storage).
+
+The default outputted format is called “plain LSON”, which includes information on the Live data structures in the tree. These nodes show up in the output as objects with two properties, for example:
+
+```json
+{
+ "liveblocksType": "LiveObject",
+ "data": ...
+}
+```
+
+If you’re not interested in this information, you can use the simpler `?format=json` query param, see below.
+
+```python
+result = client.get_storage_document(
+ room_id="my-room-id",
+ # format_=...,
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ Use the `json` format to output a simplified JSON representation of the Storage tree. In that format, each LiveObject and LiveMap will be formatted as a simple JSON object, and each LiveList will be formatted as a simple JSON array. This is a lossy format because information about the original data structures is not retained, but it may be easier to work with.
+
+
+
+
+### initialize_storage_document
+
+This endpoint initializes or reinitializes a room’s Storage. The room must already exist. Calling this endpoint will disconnect all users from the room if there are any, triggering a reconnect. Corresponds to [`liveblocks.initializeStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-storage).
+
+The format of the request body is the same as what’s returned by the get Storage endpoint.
+
+For each Liveblocks data structure that you want to create, you need a JSON element having two properties:
+- `"liveblocksType"` => `"LiveObject" | "LiveList" | "LiveMap"`
+- `"data"` => contains the nested data structures (children) and data.
+
+The root’s type can only be LiveObject.
+
+A utility function, `toPlainLson` is included in `@liveblocks/client` from `1.0.9` to help convert `LiveObject`, `LiveList`, and `LiveMap` to the structure expected by the endpoint.
+
+```python
+result = client.initialize_storage_document(
+ room_id="my-room-id",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### delete_storage_document
+
+This endpoint deletes all of the room’s Storage data. Calling this endpoint will disconnect all users from the room if there are any. Corresponds to [`liveblocks.deleteStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-storage).
+
+
+```python
+client.delete_storage_document(
+ room_id="my-room-id",
+)
+```
+
+
+
+ ID of the room
+
+
+
+
+
+### patch_storage_document
+
+Applies a sequence of [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations to the room's Storage document, useful for modifying Storage. Operations are applied in order; if any operation fails, the document is not changed and a 422 response with a helpful message is returned.
+
+**Paths and data types:** Be as specific as possible with your target path. Every parent in the chain of path segments must be a LiveObject, LiveList, or LiveMap. Complex nested objects passed in `add` or `replace` operations are automatically converted to LiveObjects and LiveLists.
+
+**Performance:** For large Storage documents, applying a patch can be expensive because the full state is reconstructed on the server to apply the operations. Very large documents may not be suitable for this endpoint.
+
+For a **full guide with examples**, see [Modifying storage via REST API with JSON Patch](https://liveblocks.io/docs/guides/modifying-storage-via-rest-api-with-json-patch).
+
+```python
+client.patch_storage_document(
+ room_id="my-room-id",
+ body=...,
+)
+```
+
+
+
+ ID of the room
+
+
+
+ Request body (application/json).
+
+
+
+
+
+## Yjs
+
+### get_yjs_document
+
+This endpoint returns a JSON representation of the room’s Yjs document. Corresponds to [`liveblocks.getYjsDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc).
+
+```python
+result = client.get_yjs_document(
+ room_id="my-room-id",
+ # formatting=True,
+ # key="root",
+ # type_=...,
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ If present, YText will return formatting.
+
+
+ Returns only a single key’s value, e.g. `doc.get(key).toJSON()`.
+
+
+ Used with key to override the inferred type, i.e. `"ymap"` will return `doc.get(key, Y.Map)`.
+
+
+
+
+### send_yjs_binary_update
+
+This endpoint is used to send a Yjs binary update to the room’s Yjs document. You can use this endpoint to initialize Yjs data for the room or to update the room’s Yjs document. To send an update to a subdocument instead of the main document, pass its `guid`. Corresponds to [`liveblocks.sendYjsBinaryUpdate`](https://liveblocks.io/docs/api-reference/liveblocks-node#put-rooms-roomId-ydoc).
+
+The update is typically obtained by calling `Y.encodeStateAsUpdate(doc)`. See the [Yjs documentation](https://docs.yjs.dev/api/document-updates) for more details. When manually making this HTTP call, set the HTTP header `Content-Type` to `application/octet-stream`, and send the binary update (a `Uint8Array`) in the body of the HTTP request. This endpoint does not accept JSON, unlike most other endpoints.
+
+```python
+client.send_yjs_binary_update(
+ room_id="my-room-id",
+ body=...,
+ # guid="subdoc-guid-123",
+)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the subdocument
+
+
+ Request body (application/octet-stream).
+
+
+
+
+
+### get_yjs_document_as_binary_update
+
+This endpoint returns the room's Yjs document encoded as a single binary update. This can be used by `Y.applyUpdate(responseBody)` to get a copy of the document in your back end. See [Yjs documentation](https://docs.yjs.dev/api/document-updates) for more information on working with updates. To return a subdocument instead of the main document, pass its `guid`. Corresponds to [`liveblocks.getYjsDocumentAsBinaryUpdate`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc-binary).
+
+```python
+result = client.get_yjs_document_as_binary_update(
+ room_id="my-room-id",
+ # guid="subdoc-guid-123",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the subdocument
+
+
+
+
+### get_yjs_versions
+
+This endpoint returns a list of version history snapshots for the room's Yjs document. The versions are returned sorted by creation date, from newest to oldest.
+
+```python
+result = client.get_yjs_versions(
+ room_id="my-room-id",
+ # limit=20,
+ # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ A limit on the number of versions to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)*
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+
+
+### get_yjs_version
+
+This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update.
+
+```python
+result = client.get_yjs_version(
+ room_id="my-room-id",
+ version_id="vh_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the version
+
+
+
+
+
+### create_yjs_version
+
+This endpoint creates a new version history snapshot for the room's Yjs document.
+
+```python
+result = client.create_yjs_version(
+ room_id="my-room-id",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+
+
+## Comments
+
+### get_threads
+
+This endpoint returns the threads in the requested room. Corresponds to [`liveblocks.getThreads`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads).
+
+```python
+result = client.get_threads(
+ room_id="my-room-id",
+ # query="metadata[\"color\"]:\"blue\"",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ Query to filter threads. You can filter by `metadata` and `resolved`, for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND resolved:true`. Learn more about [filtering threads with query language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language).
+
+
+
+
+### create_thread
+
+This endpoint creates a new thread and the first comment in the thread. Corresponds to [`liveblocks.createThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads).
+
+A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `comment.body`.
+
+```json
+{
+ "version": 1,
+ "content": [
+ {
+ "type": "paragraph",
+ "children": [{ "text": "Hello " }, { "text": "world", "bold": true }]
+ }
+ ]
+}
+```
+
+`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+
+```python
+from liveblocks.models import CreateThreadRequestBody
+
+result = client.create_thread(
+ room_id="my-room-id",
+ body=CreateThreadRequestBody(
+ comment=...,
+ # metadata=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_thread
+
+This endpoint returns a thread by its ID. Corresponds to [`liveblocks.getThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId).
+
+```python
+result = client.get_thread(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+
+
+### delete_thread
+
+This endpoint deletes a thread by its ID. Corresponds to [`liveblocks.deleteThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-threads-threadId).
+
+```python
+client.delete_thread(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+
+
+### get_thread_participants
+
+**Deprecated.** Prefer using [thread subscriptions](#get-rooms-roomId-threads-threadId-subscriptions) instead.
+
+This endpoint returns the list of thread participants. It is a list of unique user IDs representing all the thread comment authors and mentioned users in comments. Corresponds to [`liveblocks.getThreadParticipants`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-participants).
+
+```python
+result = client.get_thread_participants(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+
+
+### edit_thread_metadata
+
+This endpoint edits the metadata of a thread. The metadata is a JSON object that can be used to store any information you want about the thread, in `string`, `number`, or `boolean` form. Set a property to `null` to remove it. Corresponds to [`liveblocks.editThreadMetadata`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-metadata).
+
+`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+
+```python
+from liveblocks.models import EditThreadMetadataRequestBody
+
+result = client.edit_thread_metadata(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=EditThreadMetadataRequestBody(
+ metadata=...,
+ user_id="...",
+ # updated_at=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### mark_thread_as_resolved
+
+This endpoint marks a thread as resolved. The request body must include a `userId` to identify who resolved the thread. Returns the updated thread. Corresponds to [`liveblocks.markThreadAsResolved`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-mark-as-resolved).
+
+```python
+from liveblocks.models import MarkThreadAsResolvedRequestBody
+
+result = client.mark_thread_as_resolved(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=MarkThreadAsResolvedRequestBody(
+ user_id="...",
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### mark_thread_as_unresolved
+
+This endpoint marks a thread as unresolved. The request body must include a `userId` to identify who unresolved the thread. Returns the updated thread. Corresponds to [`liveblocks.markThreadAsUnresolved`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-mark-as-unresolved).
+
+```python
+from liveblocks.models import MarkThreadAsUnresolvedRequestBody
+
+result = client.mark_thread_as_unresolved(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=MarkThreadAsUnresolvedRequestBody(
+ user_id="...",
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### subscribe_to_thread
+
+This endpoint subscribes to a thread. Corresponds to [`liveblocks.subscribeToThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-subscribe).
+
+```python
+from liveblocks.models import SubscribeToThreadRequestBody
+
+result = client.subscribe_to_thread(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=SubscribeToThreadRequestBody(
+ user_id="...",
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### unsubscribe_from_thread
+
+This endpoint unsubscribes from a thread. Corresponds to [`liveblocks.unsubscribeFromThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-unsubscribe).
+
+```python
+from liveblocks.models import UnsubscribeFromThreadRequestBody
+
+client.unsubscribe_from_thread(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=UnsubscribeFromThreadRequestBody(
+ user_id="...",
+ ),
+)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_thread_subscriptions
+
+This endpoint gets the list of subscriptions to a thread. Corresponds to [`liveblocks.getThreadSubscriptions`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-subscriptions).
+
+```python
+result = client.get_thread_subscriptions(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+
+
+### create_comment
+
+This endpoint creates a new comment, adding it as a reply to a thread. Corresponds to [`liveblocks.createComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments).
+
+A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `body`.
+
+```json
+{
+ "version": 1,
+ "content": [
+ {
+ "type": "paragraph",
+ "children": [{ "text": "Hello " }, { "text": "world", "bold": true }]
+ }
+ ]
+}
+```
+
+`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+
+```python
+from liveblocks.models import CreateCommentRequestBody
+
+result = client.create_comment(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ body=CreateCommentRequestBody(
+ user_id="...",
+ body=...,
+ # created_at=...,
+ # metadata=...,
+ # attachment_ids=[],
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_comment
+
+This endpoint returns a comment by its ID. Corresponds to [`liveblocks.getComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-comments-commentId).
+
+```python
+result = client.get_comment(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ ID of the comment
+
+
+
+
+
+### edit_comment
+
+This endpoint edits the specified comment. Corresponds to [`liveblocks.editComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId).
+
+A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `body`.
+
+```json
+{
+ "version": 1,
+ "content": [
+ {
+ "type": "paragraph",
+ "children": [{ "text": "Hello " }, { "text": "world", "bold": true }]
+ }
+ ]
+}
+```
+
+```python
+from liveblocks.models import EditCommentRequestBody
+
+result = client.edit_comment(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+ body=EditCommentRequestBody(
+ body=...,
+ # edited_at=...,
+ # metadata=...,
+ # attachment_ids=[],
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ ID of the comment
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### delete_comment
+
+This endpoint deletes a comment. A deleted comment is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.deleteComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId).
+
+```python
+client.delete_comment(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ ID of the comment
+
+
+
+
+
+### add_comment_reaction
+
+This endpoint adds a reaction to a comment. Corresponds to [`liveblocks.addCommentReaction`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction).
+
+```python
+from liveblocks.models import AddCommentReactionRequestBody
+
+result = client.add_comment_reaction(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+ body=AddCommentReactionRequestBody(
+ user_id="...",
+ emoji="...",
+ # created_at=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ ID of the comment
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### remove_comment_reaction
+
+This endpoint removes a comment reaction. A deleted comment reaction is no longer accessible from the API or the dashboard and it cannot be restored. Corresponds to [`liveblocks.removeCommentReaction`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction).
+
+```python
+client.remove_comment_reaction(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ ID of the comment
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### edit_comment_metadata
+
+This endpoint edits the metadata of a comment. The metadata is a JSON object that can be used to store any information you want about the comment, in `string`, `number`, or `boolean` form. Set a property to `null` to remove it. Corresponds to [`liveblocks.editCommentMetadata`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-metadata).
+
+`metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+
+```python
+from liveblocks.models import EditCommentMetadataRequestBody
+
+result = client.edit_comment_metadata(
+ room_id="my-room-id",
+ thread_id="th_abc123",
+ comment_id="cm_abc123",
+ body=EditCommentMetadataRequestBody(
+ metadata=...,
+ user_id="...",
+ # updated_at=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the thread
+
+
+
+ ID of the comment
+
+
+
+ Request body (application/json).
+
+
+
+
+
+## Auth
+
+### authorize_user
+
+This endpoint lets your application server (your back end) obtain a token that one of its clients (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own application’s custom authentication endpoint. When making this request, you’ll have to use your secret key.
+
+**Important:** The difference with an [ID token](#post-identify-user) is that an access token holds all the permissions, and is the source of truth. With ID tokens, permissions are set in the Liveblocks back end (through REST API calls) and "checked at the door" every time they are used to enter a room.
+
+**Note:** When using the `@liveblocks/node` package, you can use [`Liveblocks.prepareSession`](https://liveblocks.io/docs/api-reference/liveblocks-node#access-tokens) in your back end to build this request.
+
+You can pass the property `userId` in the request’s body. This can be whatever internal identifier you use for your user accounts as long as it uniquely identifies an account. The property `userId` is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId` corresponds to one MAU.
+
+Additionally, you can set custom metadata to the token, which will be publicly accessible by other clients through the `user.info` property. This is useful for storing static data like avatar images or the user’s display name.
+
+Lastly, you’ll specify the exact permissions to give to the user using the `permissions` field. This is done in an object where the keys are room names, or room name patterns (ending in a `*`), and a list of permissions to assign the user for any room that matches that name exactly (or starts with the pattern’s prefix). For tips, see [Manage permissions with access tokens](https://liveblocks.io/docs/authentication/access-token).
+
+```python
+from liveblocks.models import AuthorizeUserRequestBody
+
+result = client.authorize_user(
+ body=AuthorizeUserRequestBody(
+ user_id="...",
+ permissions=...,
+ # user_info=...,
+ # organization_id="...",
+ ),
+)
+print(result)
+```
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### identify_user
+
+This endpoint lets your application server (your back end) obtain a token that one of its clients (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own application’s custom authentication endpoint. When using this endpoint to obtain ID tokens, you should manage your permissions by assigning user and/or group permissions to rooms explicitly, see our [Manage permissions with ID tokens](https://liveblocks.io/docs/authentication/id-token) section.
+
+**Important:** The difference with an [access token](#post-authorize-user) is that an ID token doesn’t hold any permissions itself. With ID tokens, permissions are set in the Liveblocks back end (through REST API calls) and "checked at the door" every time they are used to enter a room. With access tokens, all permissions are set in the token itself, and thus controlled from your back end entirely.
+
+**Note:** When using the `@liveblocks/node` package, you can use [`Liveblocks.identifyUser`](https://liveblocks.io/docs/api-reference/liveblocks-node) in your back end to build this request.
+
+You can pass the property `userId` in the request’s body. This can be whatever internal identifier you use for your user accounts as long as it uniquely identifies an account. The property `userId` is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId` corresponds to one MAU.
+
+If you want to use group permissions, you can also declare which `groupIds` this user belongs to. The group ID values are yours, but they will have to match the group IDs you assign permissions to when assigning permissions to rooms, see [Manage permissions with ID tokens](https://liveblocks.io/docs/authentication/id-token)).
+
+Additionally, you can set custom metadata to the token, which will be publicly accessible by other clients through the `user.info` property. This is useful for storing static data like avatar images or the user’s display name.
+
+```python
+from liveblocks.models import IdentifyUserRequestBody
+
+result = client.identify_user(
+ body=IdentifyUserRequestBody(
+ user_id="...",
+ # organization_id="...",
+ # group_ids=[],
+ # user_info=...,
+ ),
+)
+print(result)
+```
+
+
+
+ Request body (application/json).
+
+
+
+
+
+## Notifications
+
+### get_inbox_notification
+
+This endpoint returns a user’s inbox notification by its ID. Corresponds to [`liveblocks.getInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications-inboxNotificationId).
+
+```python
+result = client.get_inbox_notification(
+ user_id="user-123",
+ inbox_notification_id="in_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the user
+
+
+
+ ID of the inbox notification
+
+
+
+
+
+### delete_inbox_notification
+
+This endpoint deletes a user’s inbox notification by its ID. Corresponds to [`liveblocks.deleteInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-users-userId-inbox-notifications-inboxNotificationId).
+
+```python
+client.delete_inbox_notification(
+ user_id="user-123",
+ inbox_notification_id="in_abc123",
+)
+```
+
+
+
+ ID of the user
+
+
+
+ ID of the inbox notification
+
+
+
+
+
+### get_inbox_notifications
+
+This endpoint returns all the user’s inbox notifications. Corresponds to [`liveblocks.getInboxNotifications`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications).
+
+```python
+result = client.get_inbox_notifications(
+ user_id="user-123",
+ # organization_id="org_123456789",
+ # query="metadata[\"color\"]:\"blue\"",
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+
+
+
+ ID of the user
+
+
+
+ The organization ID to filter notifications for.
+
+
+ Query to filter notifications. You can filter by `unread`, for example, `unread:true`.
+
+
+ A limit on the number of inbox notifications to be returned. The limit can range between 1 and 50, and defaults to 50. *(default: `50`)*
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+
+
+### delete_all_inbox_notifications
+
+This endpoint deletes all the user’s inbox notifications. Corresponds to [`liveblocks.deleteAllInboxNotifications`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-users-userId-inbox-notifications).
+
+```python
+client.delete_all_inbox_notifications(
+ user_id="user-123",
+)
+```
+
+
+
+ ID of the user
+
+
+
+
+
+### get_notification_settings
+
+This endpoint returns a user's notification settings for the project. Corresponds to [`liveblocks.getNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-notification-settings).
+
+```python
+result = client.get_notification_settings(
+ user_id="user-123",
+)
+print(result)
+```
+
+
+
+ ID of the user
+
+
+
+
+
+### update_notification_settings
+
+This endpoint updates a user's notification settings for the project. Corresponds to [`liveblocks.updateNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-users-userId-notification-settings).
+
+```python
+from liveblocks.models import UpdateNotificationSettingsRequestBody
+
+result = client.update_notification_settings(
+ user_id="user-123",
+ body=UpdateNotificationSettingsRequestBody(
+ # email=...,
+ # slack=...,
+ # teams=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the user
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### delete_notification_settings
+
+This endpoint deletes a user's notification settings for the project. Corresponds to [`liveblocks.deleteNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-users-userId-notification-settings).
+
+```python
+client.delete_notification_settings(
+ user_id="user-123",
+)
+```
+
+
+
+ ID of the user
+
+
+
+
+
+### get_room_subscription_settings
+
+This endpoint returns a user’s subscription settings for a specific room. Corresponds to [`liveblocks.getRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-users-userId-subscription-settings).
+
+```python
+result = client.get_room_subscription_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the user
+
+
+
+
+
+### update_room_subscription_settings
+
+This endpoint updates a user’s subscription settings for a specific room. Corresponds to [`liveblocks.updateRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-users-userId-subscription-settings).
+
+```python
+from liveblocks.models import UpdateRoomSubscriptionSettingsRequestBody
+
+result = client.update_room_subscription_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+ body=UpdateRoomSubscriptionSettingsRequestBody(
+ # threads=...,
+ # text_mentions=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the user
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### delete_room_subscription_settings
+
+This endpoint deletes a user’s subscription settings for a specific room. Corresponds to [`liveblocks.deleteRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-users-userId-subscription-settings).
+
+```python
+client.delete_room_subscription_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the user
+
+
+
+
+
+### get_user_room_subscription_settings
+
+This endpoint returns the list of a user's room subscription settings. Corresponds to [`liveblocks.getUserRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-users-userId-room-subscription-settings).
+
+```python
+result = client.get_user_room_subscription_settings(
+ user_id="user-123",
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+ # limit=20,
+ # organization_id="org_123456789",
+)
+print(result)
+```
+
+
+
+ ID of the user
+
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+ A limit on the number of elements to be returned. The limit can range between 1 and 50, and defaults to 50. *(default: `50`)*
+
+
+ The organization ID to filter room subscription settings for.
+
+
+
+
+### get_room_notification_settings
+
+**Deprecated.** Renamed to [`/subscription-settings`](get-room-subscription-settings). Read more in our [migration guide](https://liveblocks.io/docs/platform/upgrading/2.24).
+
+This endpoint returns a user’s subscription settings for a specific room. Corresponds to [`liveblocks.getRoomNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-users-userId-notification-settings).
+
+```python
+result = client.get_room_notification_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the user
+
+
+
+
+
+### update_room_notification_settings
+
+**Deprecated.** Renamed to [`/subscription-settings`](update-room-subscription-settings). Read more in our [migration guide](https://liveblocks.io/docs/platform/upgrading/2.24).
+
+This endpoint updates a user’s notification settings for a specific room. Corresponds to [`liveblocks.updateRoomNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-users-userId-notification-settings).
+
+```python
+result = client.update_room_notification_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the user
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### delete_room_notification_settings
+
+**Deprecated.** Renamed to [`/subscription-settings`](delete-room-subscription-settings). Read more in our [migration guide](https://liveblocks.io/docs/platform/upgrading/2.24).
+
+This endpoint deletes a user’s notification settings for a specific room. Corresponds to [`liveblocks.deleteRoomNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-users-userId-notification-settings).
+
+```python
+client.delete_room_notification_settings(
+ room_id="my-room-id",
+ user_id="user-123",
+)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the user
+
+
+
+
+
+### trigger_inbox_notification
+
+This endpoint triggers an inbox notification. Corresponds to [`liveblocks.triggerInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger).
+
+```python
+client.trigger_inbox_notification()
+```
+
+
+
+ Request body (application/json).
+
+
+
+
+
+## Groups
+
+### get_groups
+
+This endpoint returns a list of all groups in your project. Corresponds to [`liveblocks.getGroups`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-groups).
+
+```python
+result = client.get_groups(
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+
+
+
+ A limit on the number of groups to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)*
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+
+
+### create_group
+
+This endpoint creates a new group. Corresponds to [`liveblocks.createGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-group).
+
+```python
+result = client.create_group()
+print(result)
+```
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_group
+
+This endpoint returns a specific group by ID. Corresponds to [`liveblocks.getGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-group).
+
+```python
+result = client.get_group(
+ group_id="engineering",
+)
+print(result)
+```
+
+
+
+ The ID of the group to retrieve.
+
+
+
+
+
+### delete_group
+
+This endpoint deletes a group. Corresponds to [`liveblocks.deleteGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-group).
+
+```python
+client.delete_group(
+ group_id="engineering",
+)
+```
+
+
+
+ The ID of the group to delete.
+
+
+
+
+
+### add_group_members
+
+This endpoint adds new members to an existing group. Corresponds to [`liveblocks.addGroupMembers`](https://liveblocks.io/docs/api-reference/liveblocks-node#add-group-members).
+
+```python
+from liveblocks.models import AddGroupMembersRequestBody
+
+result = client.add_group_members(
+ group_id="engineering",
+ body=AddGroupMembersRequestBody(
+ member_ids=[],
+ ),
+)
+print(result)
+```
+
+
+
+ The ID of the group to add members to.
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### remove_group_members
+
+This endpoint removes members from an existing group. Corresponds to [`liveblocks.removeGroupMembers`](https://liveblocks.io/docs/api-reference/liveblocks-node#remove-group-members).
+
+```python
+from liveblocks.models import RemoveGroupMembersRequestBody
+
+result = client.remove_group_members(
+ group_id="engineering",
+ body=RemoveGroupMembersRequestBody(
+ member_ids=[],
+ ),
+)
+print(result)
+```
+
+
+
+ The ID of the group to remove members from.
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_user_groups
+
+This endpoint returns all groups that a specific user is a member of. Corresponds to [`liveblocks.getUserGroups`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-user-groups).
+
+```python
+result = client.get_user_groups(
+ user_id="user-123",
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+
+
+
+ The ID of the user to get groups for.
+
+
+
+ A limit on the number of groups to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)*
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+
+
+## Ai
+
+### get_ai_copilots
+
+This endpoint returns a paginated list of AI copilots. The copilots are returned sorted by creation date, from newest to oldest. Corresponds to [`liveblocks.getAiCopilots`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-ai-copilots).
+
+```python
+result = client.get_ai_copilots(
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+
+
+
+ A limit on the number of copilots to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)*
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+
+
+### create_ai_copilot
+
+This endpoint creates a new AI copilot with the given configuration. Corresponds to [`liveblocks.createAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-ai-copilot).
+
+```python
+result = client.create_ai_copilot(
+ body=...,
+)
+print(result)
+```
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_ai_copilot
+
+This endpoint returns an AI copilot by its ID. Corresponds to [`liveblocks.getAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-ai-copilot).
+
+```python
+result = client.get_ai_copilot(
+ copilot_id="cp_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the AI copilot
+
+
+
+
+
+### update_ai_copilot
+
+This endpoint updates an existing AI copilot's configuration. Corresponds to [`liveblocks.updateAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#update-ai-copilot).
+
+This endpoint returns a 422 response if the update doesn't apply due to validation failures. For example, if the existing copilot uses the "openai" provider and you attempt to update the provider model to an incompatible value for the provider, like "gemini-2.5-pro", you'll receive a 422 response with an error message explaining where the validation failed.
+
+```python
+from liveblocks.models import UpdateAiCopilotRequestBody
+
+result = client.update_ai_copilot(
+ copilot_id="cp_abc123",
+ body=UpdateAiCopilotRequestBody(
+ # name="...",
+ # description="...",
+ # system_prompt="...",
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the AI copilot
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### delete_ai_copilot
+
+This endpoint deletes an AI copilot by its ID. A deleted copilot is no longer accessible and cannot be restored. Corresponds to [`liveblocks.deleteAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-ai-copilot).
+
+```python
+client.delete_ai_copilot(
+ copilot_id="cp_abc123",
+)
+```
+
+
+
+ ID of the AI copilot
+
+
+
+
+
+### get_knowledge_sources
+
+This endpoint returns a paginated list of knowledge sources for a specific AI copilot. Corresponds to [`liveblocks.getKnowledgeSources`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-knowledge-sources).
+
+```python
+result = client.get_knowledge_sources(
+ copilot_id="cp_abc123",
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+
+
+
+ ID of the AI copilot
+
+
+
+ A limit on the number of knowledge sources to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)*
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+
+
+### get_knowledge_source
+
+This endpoint returns a specific knowledge source by its ID. Corresponds to [`liveblocks.getKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-knowledge-source).
+
+```python
+result = client.get_knowledge_source(
+ copilot_id="cp_abc123",
+ knowledge_source_id="ks_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the AI copilot
+
+
+
+ ID of the knowledge source
+
+
+
+
+
+### create_web_knowledge_source
+
+This endpoint creates a web knowledge source for an AI copilot. This allows the copilot to access and learn from web content. Corresponds to [`liveblocks.createWebKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-web-knowledge-source).
+
+```python
+from liveblocks.models import CreateWebKnowledgeSourceRequestBody
+
+result = client.create_web_knowledge_source(
+ copilot_id="cp_abc123",
+ body=CreateWebKnowledgeSourceRequestBody(
+ copilot_id="...",
+ url="...",
+ type_=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the AI copilot
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### create_file_knowledge_source
+
+This endpoint creates a file knowledge source for an AI copilot by uploading a file. The copilot can then reference the content of the file when responding. Corresponds to [`liveblocks.createFileKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-file-knowledge-source).
+
+```python
+result = client.create_file_knowledge_source(
+ copilot_id="cp_abc123",
+ name="document.pdf",
+ body=...,
+)
+print(result)
+```
+
+
+
+ ID of the AI copilot
+
+
+
+ Name of the file
+
+
+
+ Request body (application/octet-stream).
+
+
+
+
+
+### get_file_knowledge_source_markdown
+
+This endpoint returns the content of a file knowledge source as markdown. This allows you to see what content the AI copilot has access to from uploaded files. Corresponds to [`liveblocks.getFileKnowledgeSourceMarkdown`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-file-knowledge-source-markdown).
+
+```python
+result = client.get_file_knowledge_source_markdown(
+ copilot_id="cp_abc123",
+ knowledge_source_id="ks_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the AI copilot
+
+
+
+ ID of the knowledge source
+
+
+
+
+
+### delete_file_knowledge_source
+
+This endpoint deletes a file knowledge source from an AI copilot. The copilot will no longer have access to the content from this file. Corresponds to [`liveblocks.deleteFileKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-file-knowledge-source).
+
+```python
+client.delete_file_knowledge_source(
+ copilot_id="cp_abc123",
+ knowledge_source_id="ks_abc123",
+)
+```
+
+
+
+ ID of the AI copilot
+
+
+
+ ID of the knowledge source
+
+
+
+
+
+### delete_web_knowledge_source
+
+This endpoint deletes a web knowledge source from an AI copilot. The copilot will no longer have access to the content from this source. Corresponds to [`liveblocks.deleteWebKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-web-knowledge-source).
+
+```python
+client.delete_web_knowledge_source(
+ copilot_id="cp_abc123",
+ knowledge_source_id="ks_abc123",
+)
+```
+
+
+
+ ID of the AI copilot
+
+
+
+ ID of the knowledge source
+
+
+
+
+
+### get_web_knowledge_source_links
+
+This endpoint returns a paginated list of links that were indexed from a web knowledge source. This is useful for understanding what content the AI copilot has access to from web sources. Corresponds to [`liveblocks.getWebKnowledgeSourceLinks`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-web-knowledge-source-links).
+
+```python
+result = client.get_web_knowledge_source_links(
+ copilot_id="cp_abc123",
+ knowledge_source_id="ks_abc123",
+ # limit=20,
+ # starting_after="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+
+
+
+ ID of the AI copilot
+
+
+
+ ID of the knowledge source
+
+
+
+ A limit on the number of links to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)*
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+
+
+## Management
+
+### get_management_projects
+
+Returns a paginated list of projects. You can limit the number of projects returned per page and use the provided `nextCursor` for pagination. This endpoint requires the `read:all` scope.
+
+```python
+result = client.get_management_projects(
+ # limit=20,
+ # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+
+
+
+ A limit on the number of projects to return. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)*
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+
+
+### create_management_project
+
+Creates a new project within your account. This endpoint requires the `write:all` scope. You can specify the project type, name, and version creation timeout. Upon success, returns information about the newly created project, including its ID, keys, region, and settings.
+
+```python
+from liveblocks.models import CreateManagementProjectRequestBody
+
+result = client.create_management_project(
+ body=CreateManagementProjectRequestBody(
+ type_=...,
+ # name="...",
+ # version_creation_timeout=False,
+ ),
+)
+print(result)
+```
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_management_project
+
+Returns a single project specified by its ID. This endpoint requires the `read:all` scope. If the project cannot be found, a 404 error response is returned.
+
+```python
+result = client.get_management_project(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+
+
+### update_management_project
+
+Updates an existing project specified by its ID. This endpoint allows you to modify project details such as the project name and the version creation timeout. The `versionCreationTimeout` can be set to `false` to disable the timeout or to a number of seconds between 30 and 300. Fields omitted from the request body will not be updated. Requires the `write:all` scope.
+
+If the project cannot be found, a 404 error response is returned.
+
+```python
+from liveblocks.models import UpdateManagementProjectRequestBody
+
+result = client.update_management_project(
+ project_id="683d49ed6b4d1cec5a597b13",
+ body=UpdateManagementProjectRequestBody(
+ # name="...",
+ # version_creation_timeout=False,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### delete_management_project
+
+Soft deletes the project specified by its ID. This endpoint requires the `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+```python
+client.delete_management_project(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+```
+
+
+
+ ID of the project
+
+
+
+
+
+### activate_project_public_api_key
+
+Activates the public API key associated with the specified project. This endpoint requires the `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+```python
+client.activate_project_public_api_key(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+```
+
+
+
+ ID of the project
+
+
+
+
+
+### deactivate_project_public_api_key
+
+Deactivates the public API key associated with the specified project. This endpoint requires the `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+```python
+client.deactivate_project_public_api_key(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+```
+
+
+
+ ID of the project
+
+
+
+
+
+### roll_project_public_api_key
+
+Rolls (rotates) the public API key associated with the specified project, generating a new key value while deprecating the previous one. The new key becomes immediately active. This endpoint requires the `write:all` scope.
+
+If the public key is not currently enabled for the project, a 403 error response is returned. If the project cannot be found, a 404 error response is returned. An optional `expirationIn` parameter can be provided in the request body to set when the previous key should expire.
+
+```python
+result = client.roll_project_public_api_key(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### roll_project_secret_api_key
+
+Rolls (rotates) the secret API key associated with the specified project, generating a new key value while deprecating the previous one. The new key becomes immediately active. This endpoint requires the `write:all` scope.
+
+If the project cannot be found, a 404 error response is returned. An optional `expirationIn` parameter can be provided in the request body to set when the previous key should expire.
+
+```python
+result = client.roll_project_secret_api_key(
+ project_id="683d49ed6b4d1cec5a597b13",
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_management_webhooks
+
+Returns a paginated list of webhooks for a project. This endpoint requires the `read:all` scope. The response includes an array of webhook objects associated with the specified project, as well as a `nextCursor` property for pagination. Use the `limit` query parameter to specify the maximum number of webhooks to return (1-100, default 20). If the result is paginated, use the `cursor` parameter from the `nextCursor` value in the previous response to fetch subsequent pages. If the project cannot be found, a 404 error response is returned.
+
+```python
+result = client.get_management_webhooks(
+ project_id="683d49ed6b4d1cec5a597b13",
+ # limit=20,
+ # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ A limit on the number of webhooks to return. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)*
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+
+
+### create_management_webhook
+
+Creates a new webhook for a project. This endpoint requires the `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+```python
+from liveblocks.models import CreateManagementWebhookRequestBody
+
+result = client.create_management_webhook(
+ project_id="683d49ed6b4d1cec5a597b13",
+ body=CreateManagementWebhookRequestBody(
+ url="...",
+ subscribed_events=[],
+ # rate_limit=0,
+ # additional_headers=...,
+ # storage_updated_throttle_seconds=0,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_management_webhook
+
+Get one webhook by `webhookId` for a project. Returns webhook settings such as URL, subscribed events, disabled state, throttling, and additional headers. Returns `404` if the project or webhook does not exist. This endpoint requires the `read:all` scope.
+
+```python
+result = client.get_management_webhook(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ ID of the webhook
+
+
+
+
+
+### update_management_webhook
+
+Update one webhook by `webhookId` for a project. Send only fields you want to change; omitted fields stay unchanged. Returns `404` if the project or webhook does not exist and `422` for validation errors. This endpoint requires the `write:all` scope.
+
+```python
+from liveblocks.models import UpdateManagementWebhookRequestBody
+
+result = client.update_management_webhook(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+ body=UpdateManagementWebhookRequestBody(
+ # url="...",
+ # subscribed_events=[],
+ # rate_limit=0,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ ID of the webhook
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### delete_management_webhook
+
+Delete one webhook by `webhookId` for a project. Returns `200` with an empty body on success, or `404` if the project or webhook does not exist. Requires `write:all`.
+
+```python
+client.delete_management_webhook(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+)
+```
+
+
+
+ ID of the project
+
+
+
+ ID of the webhook
+
+
+
+
+
+### roll_management_webhook_secret
+
+Rotate a webhook signing secret and return the new secret. The previous secret remains valid for 24 hours. Returns `404` if the project or webhook does not exist. This endpoint requires the `write:all` scope.
+
+```python
+result = client.roll_management_webhook_secret(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ ID of the webhook
+
+
+
+
+
+### get_management_webhook_additional_headers
+
+Get a webhook's additional headers. Returns `404` if the project or webhook does not exist. Requires `read:all`.
+
+```python
+result = client.get_management_webhook_additional_headers(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ ID of the webhook
+
+
+
+
+
+### upsert_management_webhook_additional_headers
+
+Upsert additional headers for a webhook. Provided headers are merged with existing headers, and existing values are overwritten when names match. Returns updated headers, or `404` if the project or webhook does not exist. This endpoint requires the `write:all` scope.
+
+```python
+from liveblocks.models import UpsertManagementWebhookHeadersRequestBody
+
+result = client.upsert_management_webhook_additional_headers(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+ body=UpsertManagementWebhookHeadersRequestBody(
+ headers=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ ID of the webhook
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### delete_management_webhook_additional_headers
+
+Remove selected additional headers from a webhook. Send header names in `headers` field; other headers are unchanged. Returns updated headers, or `404` if the project or webhook does not exist. This endpoint requires the `write:all` scope. At least one header name must be provided; otherwise, a 422 error response is returned.
+
+```python
+from liveblocks.models import DeleteManagementWebhookHeadersRequestBody
+
+result = client.delete_management_webhook_additional_headers(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+ body=DeleteManagementWebhookHeadersRequestBody(
+ headers=[],
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ ID of the webhook
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### recover_failed_webhook_messages
+
+Requeue failed deliveries for a webhook from the given `since` timestamp. Returns `200` with an empty body when recovery starts, an `404` if the project or webhook does not exist. This endpoint requires the `write:all` scope.
+
+```python
+from liveblocks.models import RecoverManagementWebhookFailedMessagesRequestBody
+
+client.recover_failed_webhook_messages(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+ body=RecoverManagementWebhookFailedMessagesRequestBody(
+ since=...,
+ ),
+)
+```
+
+
+
+ ID of the project
+
+
+
+ ID of the webhook
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### send_test_webhook
+
+Send a test event to a webhook and return the created message metadata. `subscribedEvent` must be one of the webhook's subscribed events, otherwise the endpoint returns `422`. Returns `404` if the project or webhook does not exist. This endpoint requires the `write:all` scope.
+
+```python
+from liveblocks.models import TestManagementWebhookRequestBody
+
+result = client.send_test_webhook(
+ project_id="683d49ed6b4d1cec5a597b13",
+ webhook_id="wh_abc123",
+ body=TestManagementWebhookRequestBody(
+ subscribed_event=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the project
+
+
+
+ ID of the webhook
+
+
+
+ Request body (application/json).
+
+
+
+
+
+
+## Error Handling
+
+All API methods raise `errors.LiveblocksError` when the server returns a non-2xx status code. You can catch and inspect these errors:
+
+```python
+from liveblocks import errors, Liveblocks
+
+client = Liveblocks(secret="sk_your_secret_key")
+
+with client:
+ try:
+ room = client.get_room(room_id="my-room")
+ except errors.LiveblocksError as e:
+ print(f"API error: {e}")
+```
+
+Methods also raise `httpx.TimeoutException` if the request exceeds the timeout.
\ No newline at end of file
diff --git a/packages/liveblocks-python/liveblocks/__init__.py b/packages/liveblocks-python/liveblocks/__init__.py
new file mode 100644
index 0000000000..ab8ee2b807
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/__init__.py
@@ -0,0 +1,12 @@
+"""A client library for accessing Liveblocks API"""
+
+from .client import AsyncLiveblocks, Liveblocks
+from .errors import LiveblocksError
+from .webhooks import WebhookHandler
+
+__all__ = (
+ "AsyncLiveblocks",
+ "Liveblocks",
+ "LiveblocksError",
+ "WebhookHandler",
+)
diff --git a/packages/liveblocks-python/liveblocks/api/__init__.py b/packages/liveblocks-python/liveblocks/api/__init__.py
new file mode 100644
index 0000000000..81f9fa241b
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/__init__.py
@@ -0,0 +1 @@
+"""Contains methods for accessing the API"""
diff --git a/packages/liveblocks-python/liveblocks/api/ai/__init__.py b/packages/liveblocks-python/liveblocks/api/ai/__init__.py
new file mode 100644
index 0000000000..2d7c0b23da
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
diff --git a/packages/liveblocks-python/liveblocks/api/ai/create_ai_copilot.py b/packages/liveblocks-python/liveblocks/api/ai/create_ai_copilot.py
new file mode 100644
index 0000000000..4bd6ca2525
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/create_ai_copilot.py
@@ -0,0 +1,124 @@
+from typing import Any
+
+import httpx
+
+from ... import errors
+from ...models.ai_copilot_anthropic import AiCopilotAnthropic
+from ...models.ai_copilot_google import AiCopilotGoogle
+from ...models.ai_copilot_open_ai import AiCopilotOpenAi
+from ...models.ai_copilot_open_ai_compatible import AiCopilotOpenAiCompatible
+from ...models.create_ai_copilot_options_anthropic import CreateAiCopilotOptionsAnthropic
+from ...models.create_ai_copilot_options_google import CreateAiCopilotOptionsGoogle
+from ...models.create_ai_copilot_options_open_ai import CreateAiCopilotOptionsOpenAi
+from ...models.create_ai_copilot_options_open_ai_compatible import CreateAiCopilotOptionsOpenAiCompatible
+
+
+def _get_kwargs(
+ *,
+ body: CreateAiCopilotOptionsAnthropic
+ | CreateAiCopilotOptionsGoogle
+ | CreateAiCopilotOptionsOpenAi
+ | CreateAiCopilotOptionsOpenAiCompatible,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/ai/copilots",
+ }
+
+ if isinstance(body, CreateAiCopilotOptionsOpenAi):
+ _kwargs["json"] = body.to_dict()
+ elif isinstance(body, CreateAiCopilotOptionsAnthropic):
+ _kwargs["json"] = body.to_dict()
+ elif isinstance(body, CreateAiCopilotOptionsGoogle):
+ _kwargs["json"] = body.to_dict()
+ else:
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(
+ *, response: httpx.Response
+) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ if response.status_code == 201:
+
+ def _parse_response_201(
+ data: object,
+ ) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_0 = AiCopilotOpenAi.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_1 = AiCopilotAnthropic.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_1
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_2 = AiCopilotGoogle.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_2
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_3 = AiCopilotOpenAiCompatible.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_3
+
+ response_201 = _parse_response_201(response.json())
+
+ return response_201
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ *,
+ client: httpx.Client,
+ body: CreateAiCopilotOptionsAnthropic
+ | CreateAiCopilotOptionsGoogle
+ | CreateAiCopilotOptionsOpenAi
+ | CreateAiCopilotOptionsOpenAiCompatible,
+) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ *,
+ client: httpx.AsyncClient,
+ body: CreateAiCopilotOptionsAnthropic
+ | CreateAiCopilotOptionsGoogle
+ | CreateAiCopilotOptionsOpenAi
+ | CreateAiCopilotOptionsOpenAiCompatible,
+) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/create_file_knowledge_source.py b/packages/liveblocks-python/liveblocks/api/ai/create_file_knowledge_source.py
new file mode 100644
index 0000000000..d06631d97b
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/create_file_knowledge_source.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.create_file_knowledge_source_response_200 import CreateFileKnowledgeSourceResponse200
+from ...types import File
+
+
+def _get_kwargs(
+ copilot_id: str,
+ name: str,
+ *,
+ body: File,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "put",
+ "url": "/v2/ai/copilots/{copilot_id}/knowledge/file/{name}".format(
+ copilot_id=quote(str(copilot_id), safe=""),
+ name=quote(str(name), safe=""),
+ ),
+ }
+
+ _kwargs["content"] = body.payload
+
+ headers["Content-Type"] = "application/octet-stream"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> CreateFileKnowledgeSourceResponse200:
+ if response.status_code == 200:
+ response_200 = CreateFileKnowledgeSourceResponse200.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ copilot_id: str,
+ name: str,
+ *,
+ client: httpx.Client,
+ body: File,
+) -> CreateFileKnowledgeSourceResponse200:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ name=name,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ copilot_id: str,
+ name: str,
+ *,
+ client: httpx.AsyncClient,
+ body: File,
+) -> CreateFileKnowledgeSourceResponse200:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ name=name,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/create_web_knowledge_source.py b/packages/liveblocks-python/liveblocks/api/ai/create_web_knowledge_source.py
new file mode 100644
index 0000000000..6a49a790c8
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/create_web_knowledge_source.py
@@ -0,0 +1,74 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.create_web_knowledge_source_request_body import CreateWebKnowledgeSourceRequestBody
+from ...models.create_web_knowledge_source_response import CreateWebKnowledgeSourceResponse
+
+
+def _get_kwargs(
+ copilot_id: str,
+ *,
+ body: CreateWebKnowledgeSourceRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/ai/copilots/{copilot_id}/knowledge/web".format(
+ copilot_id=quote(str(copilot_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> CreateWebKnowledgeSourceResponse:
+ if response.status_code == 200:
+ response_200 = CreateWebKnowledgeSourceResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ copilot_id: str,
+ *,
+ client: httpx.Client,
+ body: CreateWebKnowledgeSourceRequestBody,
+) -> CreateWebKnowledgeSourceResponse:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ copilot_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: CreateWebKnowledgeSourceRequestBody,
+) -> CreateWebKnowledgeSourceResponse:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/delete_ai_copilot.py b/packages/liveblocks-python/liveblocks/api/ai/delete_ai_copilot.py
new file mode 100644
index 0000000000..4672b61ad1
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/delete_ai_copilot.py
@@ -0,0 +1,58 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ copilot_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/ai/copilots/{copilot_id}".format(
+ copilot_id=quote(str(copilot_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ copilot_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ copilot_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/delete_file_knowledge_source.py b/packages/liveblocks-python/liveblocks/api/ai/delete_file_knowledge_source.py
new file mode 100644
index 0000000000..a95efb0286
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/delete_file_knowledge_source.py
@@ -0,0 +1,64 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ copilot_id: str,
+ knowledge_source_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/ai/copilots/{copilot_id}/knowledge/file/{knowledge_source_id}".format(
+ copilot_id=quote(str(copilot_id), safe=""),
+ knowledge_source_id=quote(str(knowledge_source_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/delete_web_knowledge_source.py b/packages/liveblocks-python/liveblocks/api/ai/delete_web_knowledge_source.py
new file mode 100644
index 0000000000..5eb604f59b
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/delete_web_knowledge_source.py
@@ -0,0 +1,64 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ copilot_id: str,
+ knowledge_source_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/ai/copilots/{copilot_id}/knowledge/web/{knowledge_source_id}".format(
+ copilot_id=quote(str(copilot_id), safe=""),
+ knowledge_source_id=quote(str(knowledge_source_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/get_ai_copilot.py b/packages/liveblocks-python/liveblocks/api/ai/get_ai_copilot.py
new file mode 100644
index 0000000000..1db565330e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/get_ai_copilot.py
@@ -0,0 +1,100 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.ai_copilot_anthropic import AiCopilotAnthropic
+from ...models.ai_copilot_google import AiCopilotGoogle
+from ...models.ai_copilot_open_ai import AiCopilotOpenAi
+from ...models.ai_copilot_open_ai_compatible import AiCopilotOpenAiCompatible
+
+
+def _get_kwargs(
+ copilot_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/ai/copilots/{copilot_id}".format(
+ copilot_id=quote(str(copilot_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(
+ *, response: httpx.Response
+) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ if response.status_code == 200:
+
+ def _parse_response_200(
+ data: object,
+ ) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_0 = AiCopilotOpenAi.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_1 = AiCopilotAnthropic.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_1
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_2 = AiCopilotGoogle.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_2
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_3 = AiCopilotOpenAiCompatible.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_3
+
+ response_200 = _parse_response_200(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ copilot_id: str,
+ *,
+ client: httpx.Client,
+) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ copilot_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/get_ai_copilots.py b/packages/liveblocks-python/liveblocks/api/ai/get_ai_copilots.py
new file mode 100644
index 0000000000..add6147099
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/get_ai_copilots.py
@@ -0,0 +1,74 @@
+from typing import Any
+
+import httpx
+
+from ... import errors
+from ...models.get_ai_copilots_response import GetAiCopilotsResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["limit"] = limit
+
+ params["startingAfter"] = starting_after
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/ai/copilots",
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetAiCopilotsResponse:
+ if response.status_code == 200:
+ response_200 = GetAiCopilotsResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ *,
+ client: httpx.Client,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> GetAiCopilotsResponse:
+ kwargs = _get_kwargs(
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ *,
+ client: httpx.AsyncClient,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> GetAiCopilotsResponse:
+ kwargs = _get_kwargs(
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/get_file_knowledge_source_markdown.py b/packages/liveblocks-python/liveblocks/api/ai/get_file_knowledge_source_markdown.py
new file mode 100644
index 0000000000..80ade17860
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/get_file_knowledge_source_markdown.py
@@ -0,0 +1,67 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_file_knowledge_source_markdown_response import GetFileKnowledgeSourceMarkdownResponse
+
+
+def _get_kwargs(
+ copilot_id: str,
+ knowledge_source_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/ai/copilots/{copilot_id}/knowledge/file/{knowledge_source_id}".format(
+ copilot_id=quote(str(copilot_id), safe=""),
+ knowledge_source_id=quote(str(knowledge_source_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetFileKnowledgeSourceMarkdownResponse:
+ if response.status_code == 200:
+ response_200 = GetFileKnowledgeSourceMarkdownResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ client: httpx.Client,
+) -> GetFileKnowledgeSourceMarkdownResponse:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> GetFileKnowledgeSourceMarkdownResponse:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/get_knowledge_source.py b/packages/liveblocks-python/liveblocks/api/ai/get_knowledge_source.py
new file mode 100644
index 0000000000..2127d67252
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/get_knowledge_source.py
@@ -0,0 +1,84 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.knowledge_source_file_source import KnowledgeSourceFileSource
+from ...models.knowledge_source_web_source import KnowledgeSourceWebSource
+
+
+def _get_kwargs(
+ copilot_id: str,
+ knowledge_source_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/ai/copilots/{copilot_id}/knowledge/{knowledge_source_id}".format(
+ copilot_id=quote(str(copilot_id), safe=""),
+ knowledge_source_id=quote(str(knowledge_source_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> KnowledgeSourceFileSource | KnowledgeSourceWebSource:
+ if response.status_code == 200:
+
+ def _parse_response_200(data: object) -> KnowledgeSourceFileSource | KnowledgeSourceWebSource:
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_knowledge_source_type_0 = KnowledgeSourceWebSource.from_dict(data)
+
+ return componentsschemas_knowledge_source_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_knowledge_source_type_1 = KnowledgeSourceFileSource.from_dict(data)
+
+ return componentsschemas_knowledge_source_type_1
+
+ response_200 = _parse_response_200(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ client: httpx.Client,
+) -> KnowledgeSourceFileSource | KnowledgeSourceWebSource:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> KnowledgeSourceFileSource | KnowledgeSourceWebSource:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/get_knowledge_sources.py b/packages/liveblocks-python/liveblocks/api/ai/get_knowledge_sources.py
new file mode 100644
index 0000000000..caf04e121c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/get_knowledge_sources.py
@@ -0,0 +1,82 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_knowledge_sources_response import GetKnowledgeSourcesResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ copilot_id: str,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["limit"] = limit
+
+ params["startingAfter"] = starting_after
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/ai/copilots/{copilot_id}/knowledge".format(
+ copilot_id=quote(str(copilot_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetKnowledgeSourcesResponse:
+ if response.status_code == 200:
+ response_200 = GetKnowledgeSourcesResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ copilot_id: str,
+ *,
+ client: httpx.Client,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> GetKnowledgeSourcesResponse:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ copilot_id: str,
+ *,
+ client: httpx.AsyncClient,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> GetKnowledgeSourcesResponse:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/get_web_knowledge_source_links.py b/packages/liveblocks-python/liveblocks/api/ai/get_web_knowledge_source_links.py
new file mode 100644
index 0000000000..84630564f2
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/get_web_knowledge_source_links.py
@@ -0,0 +1,88 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_web_knowledge_source_links_response import GetWebKnowledgeSourceLinksResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["limit"] = limit
+
+ params["startingAfter"] = starting_after
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/ai/copilots/{copilot_id}/knowledge/web/{knowledge_source_id}/links".format(
+ copilot_id=quote(str(copilot_id), safe=""),
+ knowledge_source_id=quote(str(knowledge_source_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetWebKnowledgeSourceLinksResponse:
+ if response.status_code == 200:
+ response_200 = GetWebKnowledgeSourceLinksResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ client: httpx.Client,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> GetWebKnowledgeSourceLinksResponse:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ client: httpx.AsyncClient,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> GetWebKnowledgeSourceLinksResponse:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/ai/update_ai_copilot.py b/packages/liveblocks-python/liveblocks/api/ai/update_ai_copilot.py
new file mode 100644
index 0000000000..de0440e6c0
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/ai/update_ai_copilot.py
@@ -0,0 +1,113 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.ai_copilot_anthropic import AiCopilotAnthropic
+from ...models.ai_copilot_google import AiCopilotGoogle
+from ...models.ai_copilot_open_ai import AiCopilotOpenAi
+from ...models.ai_copilot_open_ai_compatible import AiCopilotOpenAiCompatible
+from ...models.update_ai_copilot_request_body import UpdateAiCopilotRequestBody
+
+
+def _get_kwargs(
+ copilot_id: str,
+ *,
+ body: UpdateAiCopilotRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/ai/copilots/{copilot_id}".format(
+ copilot_id=quote(str(copilot_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(
+ *, response: httpx.Response
+) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ if response.status_code == 200:
+
+ def _parse_response_200(
+ data: object,
+ ) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_0 = AiCopilotOpenAi.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_1 = AiCopilotAnthropic.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_1
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_2 = AiCopilotGoogle.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_2
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_3 = AiCopilotOpenAiCompatible.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_3
+
+ response_200 = _parse_response_200(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ copilot_id: str,
+ *,
+ client: httpx.Client,
+ body: UpdateAiCopilotRequestBody,
+) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ copilot_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UpdateAiCopilotRequestBody,
+) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ kwargs = _get_kwargs(
+ copilot_id=copilot_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/auth/__init__.py b/packages/liveblocks-python/liveblocks/api/auth/__init__.py
new file mode 100644
index 0000000000..2d7c0b23da
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/auth/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
diff --git a/packages/liveblocks-python/liveblocks/api/auth/authorize_user.py b/packages/liveblocks-python/liveblocks/api/auth/authorize_user.py
new file mode 100644
index 0000000000..91151b6804
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/auth/authorize_user.py
@@ -0,0 +1,66 @@
+from typing import Any
+
+import httpx
+
+from ... import errors
+from ...models.authorize_user_request_body import AuthorizeUserRequestBody
+from ...models.authorize_user_response import AuthorizeUserResponse
+
+
+def _get_kwargs(
+ *,
+ body: AuthorizeUserRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/authorize-user",
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> AuthorizeUserResponse:
+ if response.status_code == 200:
+ response_200 = AuthorizeUserResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ *,
+ client: httpx.Client,
+ body: AuthorizeUserRequestBody,
+) -> AuthorizeUserResponse:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ *,
+ client: httpx.AsyncClient,
+ body: AuthorizeUserRequestBody,
+) -> AuthorizeUserResponse:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/auth/identify_user.py b/packages/liveblocks-python/liveblocks/api/auth/identify_user.py
new file mode 100644
index 0000000000..efd59664af
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/auth/identify_user.py
@@ -0,0 +1,66 @@
+from typing import Any
+
+import httpx
+
+from ... import errors
+from ...models.identify_user_request_body import IdentifyUserRequestBody
+from ...models.identify_user_response import IdentifyUserResponse
+
+
+def _get_kwargs(
+ *,
+ body: IdentifyUserRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/identify-user",
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> IdentifyUserResponse:
+ if response.status_code == 200:
+ response_200 = IdentifyUserResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ *,
+ client: httpx.Client,
+ body: IdentifyUserRequestBody,
+) -> IdentifyUserResponse:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ *,
+ client: httpx.AsyncClient,
+ body: IdentifyUserRequestBody,
+) -> IdentifyUserResponse:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/__init__.py b/packages/liveblocks-python/liveblocks/api/comments/__init__.py
new file mode 100644
index 0000000000..2d7c0b23da
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
diff --git a/packages/liveblocks-python/liveblocks/api/comments/add_comment_reaction.py b/packages/liveblocks-python/liveblocks/api/comments/add_comment_reaction.py
new file mode 100644
index 0000000000..5e259e12e0
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/add_comment_reaction.py
@@ -0,0 +1,86 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.add_comment_reaction_request_body import AddCommentReactionRequestBody
+from ...models.comment_reaction import CommentReaction
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: AddCommentReactionRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/comments/{comment_id}/add-reaction".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ comment_id=quote(str(comment_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> CommentReaction:
+ if response.status_code == 200:
+ response_200 = CommentReaction.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.Client,
+ body: AddCommentReactionRequestBody,
+) -> CommentReaction:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: AddCommentReactionRequestBody,
+) -> CommentReaction:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/create_comment.py b/packages/liveblocks-python/liveblocks/api/comments/create_comment.py
new file mode 100644
index 0000000000..ec0a425a9a
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/create_comment.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.comment import Comment
+from ...models.create_comment_request_body import CreateCommentRequestBody
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ *,
+ body: CreateCommentRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/comments".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Comment:
+ if response.status_code == 200:
+ response_200 = Comment.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.Client,
+ body: CreateCommentRequestBody,
+) -> Comment:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: CreateCommentRequestBody,
+) -> Comment:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/create_thread.py b/packages/liveblocks-python/liveblocks/api/comments/create_thread.py
new file mode 100644
index 0000000000..0fb7be8afb
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/create_thread.py
@@ -0,0 +1,74 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.create_thread_request_body import CreateThreadRequestBody
+from ...models.thread import Thread
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ body: CreateThreadRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/threads".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Thread:
+ if response.status_code == 200:
+ response_200 = Thread.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ body: CreateThreadRequestBody,
+) -> Thread:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: CreateThreadRequestBody,
+) -> Thread:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/delete_comment.py b/packages/liveblocks-python/liveblocks/api/comments/delete_comment.py
new file mode 100644
index 0000000000..2eb4f3db16
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/delete_comment.py
@@ -0,0 +1,70 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/comments/{comment_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ comment_id=quote(str(comment_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/delete_thread.py b/packages/liveblocks-python/liveblocks/api/comments/delete_thread.py
new file mode 100644
index 0000000000..b46662fcc4
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/delete_thread.py
@@ -0,0 +1,64 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/edit_comment.py b/packages/liveblocks-python/liveblocks/api/comments/edit_comment.py
new file mode 100644
index 0000000000..252d897737
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/edit_comment.py
@@ -0,0 +1,86 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.comment import Comment
+from ...models.edit_comment_request_body import EditCommentRequestBody
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: EditCommentRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/comments/{comment_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ comment_id=quote(str(comment_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Comment:
+ if response.status_code == 200:
+ response_200 = Comment.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.Client,
+ body: EditCommentRequestBody,
+) -> Comment:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: EditCommentRequestBody,
+) -> Comment:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/edit_comment_metadata.py b/packages/liveblocks-python/liveblocks/api/comments/edit_comment_metadata.py
new file mode 100644
index 0000000000..36ccdd1ec8
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/edit_comment_metadata.py
@@ -0,0 +1,86 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.comment_metadata import CommentMetadata
+from ...models.edit_comment_metadata_request_body import EditCommentMetadataRequestBody
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: EditCommentMetadataRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/comments/{comment_id}/metadata".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ comment_id=quote(str(comment_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> CommentMetadata:
+ if response.status_code == 200:
+ response_200 = CommentMetadata.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.Client,
+ body: EditCommentMetadataRequestBody,
+) -> CommentMetadata:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: EditCommentMetadataRequestBody,
+) -> CommentMetadata:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/edit_thread_metadata.py b/packages/liveblocks-python/liveblocks/api/comments/edit_thread_metadata.py
new file mode 100644
index 0000000000..a0506a1c8e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/edit_thread_metadata.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.edit_thread_metadata_request_body import EditThreadMetadataRequestBody
+from ...models.thread_metadata import ThreadMetadata
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ *,
+ body: EditThreadMetadataRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/metadata".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> ThreadMetadata:
+ if response.status_code == 200:
+ response_200 = ThreadMetadata.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.Client,
+ body: EditThreadMetadataRequestBody,
+) -> ThreadMetadata:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: EditThreadMetadataRequestBody,
+) -> ThreadMetadata:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/get_comment.py b/packages/liveblocks-python/liveblocks/api/comments/get_comment.py
new file mode 100644
index 0000000000..2655f065e5
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/get_comment.py
@@ -0,0 +1,73 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.comment import Comment
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/comments/{comment_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ comment_id=quote(str(comment_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Comment:
+ if response.status_code == 200:
+ response_200 = Comment.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.Client,
+) -> Comment:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> Comment:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/get_thread.py b/packages/liveblocks-python/liveblocks/api/comments/get_thread.py
new file mode 100644
index 0000000000..0557c711bd
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/get_thread.py
@@ -0,0 +1,67 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.thread import Thread
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Thread:
+ if response.status_code == 200:
+ response_200 = Thread.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.Client,
+) -> Thread:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> Thread:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/get_thread_participants.py b/packages/liveblocks-python/liveblocks/api/comments/get_thread_participants.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/liveblocks-python/liveblocks/api/comments/get_thread_subscriptions.py b/packages/liveblocks-python/liveblocks/api/comments/get_thread_subscriptions.py
new file mode 100644
index 0000000000..c7aa1f0a78
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/get_thread_subscriptions.py
@@ -0,0 +1,67 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_thread_subscriptions_response import GetThreadSubscriptionsResponse
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/subscriptions".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetThreadSubscriptionsResponse:
+ if response.status_code == 200:
+ response_200 = GetThreadSubscriptionsResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.Client,
+) -> GetThreadSubscriptionsResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> GetThreadSubscriptionsResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/get_threads.py b/packages/liveblocks-python/liveblocks/api/comments/get_threads.py
new file mode 100644
index 0000000000..db22db4490
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/get_threads.py
@@ -0,0 +1,75 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_threads_response import GetThreadsResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ query: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["query"] = query
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/threads".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetThreadsResponse:
+ if response.status_code == 200:
+ response_200 = GetThreadsResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ query: str | Unset = UNSET,
+) -> GetThreadsResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ query=query,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ query: str | Unset = UNSET,
+) -> GetThreadsResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ query=query,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/mark_thread_as_resolved.py b/packages/liveblocks-python/liveblocks/api/comments/mark_thread_as_resolved.py
new file mode 100644
index 0000000000..b5c4587e58
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/mark_thread_as_resolved.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.mark_thread_as_resolved_request_body import MarkThreadAsResolvedRequestBody
+from ...models.thread import Thread
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ *,
+ body: MarkThreadAsResolvedRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/mark-as-resolved".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Thread:
+ if response.status_code == 200:
+ response_200 = Thread.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.Client,
+ body: MarkThreadAsResolvedRequestBody,
+) -> Thread:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: MarkThreadAsResolvedRequestBody,
+) -> Thread:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/mark_thread_as_unresolved.py b/packages/liveblocks-python/liveblocks/api/comments/mark_thread_as_unresolved.py
new file mode 100644
index 0000000000..8d923d61ff
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/mark_thread_as_unresolved.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.mark_thread_as_unresolved_request_body import MarkThreadAsUnresolvedRequestBody
+from ...models.thread import Thread
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ *,
+ body: MarkThreadAsUnresolvedRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/mark-as-unresolved".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Thread:
+ if response.status_code == 200:
+ response_200 = Thread.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.Client,
+ body: MarkThreadAsUnresolvedRequestBody,
+) -> Thread:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: MarkThreadAsUnresolvedRequestBody,
+) -> Thread:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/remove_comment_reaction.py b/packages/liveblocks-python/liveblocks/api/comments/remove_comment_reaction.py
new file mode 100644
index 0000000000..e32dad3780
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/remove_comment_reaction.py
@@ -0,0 +1,85 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.remove_comment_reaction_request_body import RemoveCommentReactionRequestBody
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: RemoveCommentReactionRequestBody | Unset = UNSET,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/comments/{comment_id}/remove-reaction".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ comment_id=quote(str(comment_id), safe=""),
+ ),
+ }
+
+ if not isinstance(body, Unset):
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.Client,
+ body: RemoveCommentReactionRequestBody | Unset = UNSET,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: RemoveCommentReactionRequestBody | Unset = UNSET,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/subscribe_to_thread.py b/packages/liveblocks-python/liveblocks/api/comments/subscribe_to_thread.py
new file mode 100644
index 0000000000..a2256bd079
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/subscribe_to_thread.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.subscribe_to_thread_request_body import SubscribeToThreadRequestBody
+from ...models.subscription import Subscription
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ *,
+ body: SubscribeToThreadRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/subscribe".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Subscription:
+ if response.status_code == 200:
+ response_200 = Subscription.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.Client,
+ body: SubscribeToThreadRequestBody,
+) -> Subscription:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: SubscribeToThreadRequestBody,
+) -> Subscription:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/comments/unsubscribe_from_thread.py b/packages/liveblocks-python/liveblocks/api/comments/unsubscribe_from_thread.py
new file mode 100644
index 0000000000..5bd7be00ce
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/comments/unsubscribe_from_thread.py
@@ -0,0 +1,77 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.unsubscribe_from_thread_request_body import UnsubscribeFromThreadRequestBody
+
+
+def _get_kwargs(
+ room_id: str,
+ thread_id: str,
+ *,
+ body: UnsubscribeFromThreadRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/threads/{thread_id}/unsubscribe".format(
+ room_id=quote(str(room_id), safe=""),
+ thread_id=quote(str(thread_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 200:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.Client,
+ body: UnsubscribeFromThreadRequestBody,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ thread_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UnsubscribeFromThreadRequestBody,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/groups/__init__.py b/packages/liveblocks-python/liveblocks/api/groups/__init__.py
new file mode 100644
index 0000000000..2d7c0b23da
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/groups/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
diff --git a/packages/liveblocks-python/liveblocks/api/groups/add_group_members.py b/packages/liveblocks-python/liveblocks/api/groups/add_group_members.py
new file mode 100644
index 0000000000..ed3a85eee6
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/groups/add_group_members.py
@@ -0,0 +1,74 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.add_group_members_request_body import AddGroupMembersRequestBody
+from ...models.group import Group
+
+
+def _get_kwargs(
+ group_id: str,
+ *,
+ body: AddGroupMembersRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/groups/{group_id}/add-members".format(
+ group_id=quote(str(group_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Group:
+ if response.status_code == 200:
+ response_200 = Group.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ group_id: str,
+ *,
+ client: httpx.Client,
+ body: AddGroupMembersRequestBody,
+) -> Group:
+ kwargs = _get_kwargs(
+ group_id=group_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ group_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: AddGroupMembersRequestBody,
+) -> Group:
+ kwargs = _get_kwargs(
+ group_id=group_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/groups/create_group.py b/packages/liveblocks-python/liveblocks/api/groups/create_group.py
new file mode 100644
index 0000000000..06d3b58995
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/groups/create_group.py
@@ -0,0 +1,68 @@
+from typing import Any
+
+import httpx
+
+from ... import errors
+from ...models.create_group_request_body import CreateGroupRequestBody
+from ...models.group import Group
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ *,
+ body: CreateGroupRequestBody | Unset = UNSET,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/groups",
+ }
+
+ if not isinstance(body, Unset):
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Group:
+ if response.status_code == 200:
+ response_200 = Group.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ *,
+ client: httpx.Client,
+ body: CreateGroupRequestBody | Unset = UNSET,
+) -> Group:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ *,
+ client: httpx.AsyncClient,
+ body: CreateGroupRequestBody | Unset = UNSET,
+) -> Group:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/groups/delete_group.py b/packages/liveblocks-python/liveblocks/api/groups/delete_group.py
new file mode 100644
index 0000000000..580bc69bfe
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/groups/delete_group.py
@@ -0,0 +1,58 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ group_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/groups/{group_id}".format(
+ group_id=quote(str(group_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ group_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ group_id=group_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ group_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ group_id=group_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/groups/get_group.py b/packages/liveblocks-python/liveblocks/api/groups/get_group.py
new file mode 100644
index 0000000000..f12834e8f1
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/groups/get_group.py
@@ -0,0 +1,61 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.group import Group
+
+
+def _get_kwargs(
+ group_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/groups/{group_id}".format(
+ group_id=quote(str(group_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Group:
+ if response.status_code == 200:
+ response_200 = Group.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ group_id: str,
+ *,
+ client: httpx.Client,
+) -> Group:
+ kwargs = _get_kwargs(
+ group_id=group_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ group_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> Group:
+ kwargs = _get_kwargs(
+ group_id=group_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/groups/get_groups.py b/packages/liveblocks-python/liveblocks/api/groups/get_groups.py
new file mode 100644
index 0000000000..b366a35176
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/groups/get_groups.py
@@ -0,0 +1,74 @@
+from typing import Any
+
+import httpx
+
+from ... import errors
+from ...models.get_groups_response import GetGroupsResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["limit"] = limit
+
+ params["startingAfter"] = starting_after
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/groups",
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetGroupsResponse:
+ if response.status_code == 200:
+ response_200 = GetGroupsResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ *,
+ client: httpx.Client,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> GetGroupsResponse:
+ kwargs = _get_kwargs(
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ *,
+ client: httpx.AsyncClient,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> GetGroupsResponse:
+ kwargs = _get_kwargs(
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/groups/get_user_groups.py b/packages/liveblocks-python/liveblocks/api/groups/get_user_groups.py
new file mode 100644
index 0000000000..100d20f295
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/groups/get_user_groups.py
@@ -0,0 +1,82 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_user_groups_response import GetUserGroupsResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ user_id: str,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["limit"] = limit
+
+ params["startingAfter"] = starting_after
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/users/{user_id}/groups".format(
+ user_id=quote(str(user_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetUserGroupsResponse:
+ if response.status_code == 200:
+ response_200 = GetUserGroupsResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ user_id: str,
+ *,
+ client: httpx.Client,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> GetUserGroupsResponse:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ user_id: str,
+ *,
+ client: httpx.AsyncClient,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+) -> GetUserGroupsResponse:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/groups/remove_group_members.py b/packages/liveblocks-python/liveblocks/api/groups/remove_group_members.py
new file mode 100644
index 0000000000..87da227f57
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/groups/remove_group_members.py
@@ -0,0 +1,74 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.group import Group
+from ...models.remove_group_members_request_body import RemoveGroupMembersRequestBody
+
+
+def _get_kwargs(
+ group_id: str,
+ *,
+ body: RemoveGroupMembersRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/groups/{group_id}/remove-members".format(
+ group_id=quote(str(group_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Group:
+ if response.status_code == 200:
+ response_200 = Group.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ group_id: str,
+ *,
+ client: httpx.Client,
+ body: RemoveGroupMembersRequestBody,
+) -> Group:
+ kwargs = _get_kwargs(
+ group_id=group_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ group_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: RemoveGroupMembersRequestBody,
+) -> Group:
+ kwargs = _get_kwargs(
+ group_id=group_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/__init__.py b/packages/liveblocks-python/liveblocks/api/management/__init__.py
new file mode 100644
index 0000000000..2d7c0b23da
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
diff --git a/packages/liveblocks-python/liveblocks/api/management/activate_project_public_api_key.py b/packages/liveblocks-python/liveblocks/api/management/activate_project_public_api_key.py
new file mode 100644
index 0000000000..0752781599
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/activate_project_public_api_key.py
@@ -0,0 +1,58 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ project_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}/api-keys/public/activate".format(
+ project_id=quote(str(project_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 200:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/create_management_project.py b/packages/liveblocks-python/liveblocks/api/management/create_management_project.py
new file mode 100644
index 0000000000..ba010534b6
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/create_management_project.py
@@ -0,0 +1,66 @@
+from typing import Any
+
+import httpx
+
+from ... import errors
+from ...models.create_management_project_request_body import CreateManagementProjectRequestBody
+from ...models.management_project import ManagementProject
+
+
+def _get_kwargs(
+ *,
+ body: CreateManagementProjectRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects",
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> ManagementProject:
+ if response.status_code == 200:
+ response_200 = ManagementProject.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ *,
+ client: httpx.Client,
+ body: CreateManagementProjectRequestBody,
+) -> ManagementProject:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ *,
+ client: httpx.AsyncClient,
+ body: CreateManagementProjectRequestBody,
+) -> ManagementProject:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/create_management_webhook.py b/packages/liveblocks-python/liveblocks/api/management/create_management_webhook.py
new file mode 100644
index 0000000000..949c582e78
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/create_management_webhook.py
@@ -0,0 +1,74 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.create_management_webhook_request_body import CreateManagementWebhookRequestBody
+from ...models.management_webhook import ManagementWebhook
+
+
+def _get_kwargs(
+ project_id: str,
+ *,
+ body: CreateManagementWebhookRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}/webhooks".format(
+ project_id=quote(str(project_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> ManagementWebhook:
+ if response.status_code == 200:
+ response_200 = ManagementWebhook.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ *,
+ client: httpx.Client,
+ body: CreateManagementWebhookRequestBody,
+) -> ManagementWebhook:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: CreateManagementWebhookRequestBody,
+) -> ManagementWebhook:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/deactivate_project_public_api_key.py b/packages/liveblocks-python/liveblocks/api/management/deactivate_project_public_api_key.py
new file mode 100644
index 0000000000..0479e92ef5
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/deactivate_project_public_api_key.py
@@ -0,0 +1,58 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ project_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}/api-keys/public/deactivate".format(
+ project_id=quote(str(project_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 200:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/delete_management_project.py b/packages/liveblocks-python/liveblocks/api/management/delete_management_project.py
new file mode 100644
index 0000000000..94432dc9d4
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/delete_management_project.py
@@ -0,0 +1,58 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ project_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/management/projects/{project_id}".format(
+ project_id=quote(str(project_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 200:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/delete_management_webhook.py b/packages/liveblocks-python/liveblocks/api/management/delete_management_webhook.py
new file mode 100644
index 0000000000..2bde7ea2e9
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/delete_management_webhook.py
@@ -0,0 +1,64 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ project_id: str,
+ webhook_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/management/projects/{project_id}/webhooks/{webhook_id}".format(
+ project_id=quote(str(project_id), safe=""),
+ webhook_id=quote(str(webhook_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 200:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/delete_management_webhook_additional_headers.py b/packages/liveblocks-python/liveblocks/api/management/delete_management_webhook_additional_headers.py
new file mode 100644
index 0000000000..da2b7fbd22
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/delete_management_webhook_additional_headers.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.delete_management_webhook_headers_request_body import DeleteManagementWebhookHeadersRequestBody
+from ...models.delete_management_webhook_headers_response import DeleteManagementWebhookHeadersResponse
+
+
+def _get_kwargs(
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: DeleteManagementWebhookHeadersRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}/webhooks/{webhook_id}/delete-additional-headers".format(
+ project_id=quote(str(project_id), safe=""),
+ webhook_id=quote(str(webhook_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> DeleteManagementWebhookHeadersResponse:
+ if response.status_code == 200:
+ response_200 = DeleteManagementWebhookHeadersResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.Client,
+ body: DeleteManagementWebhookHeadersRequestBody,
+) -> DeleteManagementWebhookHeadersResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: DeleteManagementWebhookHeadersRequestBody,
+) -> DeleteManagementWebhookHeadersResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/get_management_project.py b/packages/liveblocks-python/liveblocks/api/management/get_management_project.py
new file mode 100644
index 0000000000..2fe807eacd
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/get_management_project.py
@@ -0,0 +1,61 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.management_project import ManagementProject
+
+
+def _get_kwargs(
+ project_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/management/projects/{project_id}".format(
+ project_id=quote(str(project_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> ManagementProject:
+ if response.status_code == 200:
+ response_200 = ManagementProject.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ *,
+ client: httpx.Client,
+) -> ManagementProject:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> ManagementProject:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/get_management_projects.py b/packages/liveblocks-python/liveblocks/api/management/get_management_projects.py
new file mode 100644
index 0000000000..cbdc841cf2
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/get_management_projects.py
@@ -0,0 +1,74 @@
+from typing import Any
+
+import httpx
+
+from ... import errors
+from ...models.get_management_projects_response import GetManagementProjectsResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ *,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["limit"] = limit
+
+ params["cursor"] = cursor
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/management/projects",
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetManagementProjectsResponse:
+ if response.status_code == 200:
+ response_200 = GetManagementProjectsResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ *,
+ client: httpx.Client,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+) -> GetManagementProjectsResponse:
+ kwargs = _get_kwargs(
+ limit=limit,
+ cursor=cursor,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ *,
+ client: httpx.AsyncClient,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+) -> GetManagementProjectsResponse:
+ kwargs = _get_kwargs(
+ limit=limit,
+ cursor=cursor,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/get_management_webhook.py b/packages/liveblocks-python/liveblocks/api/management/get_management_webhook.py
new file mode 100644
index 0000000000..c41f986265
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/get_management_webhook.py
@@ -0,0 +1,67 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.management_webhook import ManagementWebhook
+
+
+def _get_kwargs(
+ project_id: str,
+ webhook_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/management/projects/{project_id}/webhooks/{webhook_id}".format(
+ project_id=quote(str(project_id), safe=""),
+ webhook_id=quote(str(webhook_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> ManagementWebhook:
+ if response.status_code == 200:
+ response_200 = ManagementWebhook.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.Client,
+) -> ManagementWebhook:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> ManagementWebhook:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/get_management_webhook_additional_headers.py b/packages/liveblocks-python/liveblocks/api/management/get_management_webhook_additional_headers.py
new file mode 100644
index 0000000000..c8125a8968
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/get_management_webhook_additional_headers.py
@@ -0,0 +1,67 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_management_webhook_headers_response import GetManagementWebhookHeadersResponse
+
+
+def _get_kwargs(
+ project_id: str,
+ webhook_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/management/projects/{project_id}/webhooks/{webhook_id}/additional-headers".format(
+ project_id=quote(str(project_id), safe=""),
+ webhook_id=quote(str(webhook_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetManagementWebhookHeadersResponse:
+ if response.status_code == 200:
+ response_200 = GetManagementWebhookHeadersResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.Client,
+) -> GetManagementWebhookHeadersResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> GetManagementWebhookHeadersResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/get_management_webhooks.py b/packages/liveblocks-python/liveblocks/api/management/get_management_webhooks.py
new file mode 100644
index 0000000000..71b82532c5
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/get_management_webhooks.py
@@ -0,0 +1,82 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_management_webhooks_response import GetManagementWebhooksResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ project_id: str,
+ *,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["limit"] = limit
+
+ params["cursor"] = cursor
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/management/projects/{project_id}/webhooks".format(
+ project_id=quote(str(project_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetManagementWebhooksResponse:
+ if response.status_code == 200:
+ response_200 = GetManagementWebhooksResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ *,
+ client: httpx.Client,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+) -> GetManagementWebhooksResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ limit=limit,
+ cursor=cursor,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ *,
+ client: httpx.AsyncClient,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+) -> GetManagementWebhooksResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ limit=limit,
+ cursor=cursor,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/recover_failed_webhook_messages.py b/packages/liveblocks-python/liveblocks/api/management/recover_failed_webhook_messages.py
new file mode 100644
index 0000000000..8bd705579d
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/recover_failed_webhook_messages.py
@@ -0,0 +1,79 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.recover_management_webhook_failed_messages_request_body import (
+ RecoverManagementWebhookFailedMessagesRequestBody,
+)
+
+
+def _get_kwargs(
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: RecoverManagementWebhookFailedMessagesRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}/webhooks/{webhook_id}/recover-failed-messages".format(
+ project_id=quote(str(project_id), safe=""),
+ webhook_id=quote(str(webhook_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 200:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.Client,
+ body: RecoverManagementWebhookFailedMessagesRequestBody,
+) -> None:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: RecoverManagementWebhookFailedMessagesRequestBody,
+) -> None:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/roll_management_webhook_secret.py b/packages/liveblocks-python/liveblocks/api/management/roll_management_webhook_secret.py
new file mode 100644
index 0000000000..487a196e95
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/roll_management_webhook_secret.py
@@ -0,0 +1,67 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.rotate_management_webhook_secret_response import RotateManagementWebhookSecretResponse
+
+
+def _get_kwargs(
+ project_id: str,
+ webhook_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}/webhooks/{webhook_id}/secret/roll".format(
+ project_id=quote(str(project_id), safe=""),
+ webhook_id=quote(str(webhook_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> RotateManagementWebhookSecretResponse:
+ if response.status_code == 200:
+ response_200 = RotateManagementWebhookSecretResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.Client,
+) -> RotateManagementWebhookSecretResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> RotateManagementWebhookSecretResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/roll_project_public_api_key.py b/packages/liveblocks-python/liveblocks/api/management/roll_project_public_api_key.py
new file mode 100644
index 0000000000..49021caa32
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/roll_project_public_api_key.py
@@ -0,0 +1,76 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.roll_project_public_api_key_request_body import RollProjectPublicApiKeyRequestBody
+from ...models.roll_project_public_api_key_response import RollProjectPublicApiKeyResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ project_id: str,
+ *,
+ body: RollProjectPublicApiKeyRequestBody | Unset = UNSET,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}/api-keys/public/roll".format(
+ project_id=quote(str(project_id), safe=""),
+ ),
+ }
+
+ if not isinstance(body, Unset):
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> RollProjectPublicApiKeyResponse:
+ if response.status_code == 200:
+ response_200 = RollProjectPublicApiKeyResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ *,
+ client: httpx.Client,
+ body: RollProjectPublicApiKeyRequestBody | Unset = UNSET,
+) -> RollProjectPublicApiKeyResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: RollProjectPublicApiKeyRequestBody | Unset = UNSET,
+) -> RollProjectPublicApiKeyResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/roll_project_secret_api_key.py b/packages/liveblocks-python/liveblocks/api/management/roll_project_secret_api_key.py
new file mode 100644
index 0000000000..8a023e34d9
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/roll_project_secret_api_key.py
@@ -0,0 +1,78 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.management_project_roll_project_secret_api_key_response_secret_key_response import (
+ ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse,
+)
+from ...models.roll_project_secret_api_key_request_body import RollProjectSecretApiKeyRequestBody
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ project_id: str,
+ *,
+ body: RollProjectSecretApiKeyRequestBody | Unset = UNSET,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}/api-keys/secret/roll".format(
+ project_id=quote(str(project_id), safe=""),
+ ),
+ }
+
+ if not isinstance(body, Unset):
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse:
+ if response.status_code == 200:
+ response_200 = ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ *,
+ client: httpx.Client,
+ body: RollProjectSecretApiKeyRequestBody | Unset = UNSET,
+) -> ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: RollProjectSecretApiKeyRequestBody | Unset = UNSET,
+) -> ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/send_test_webhook.py b/packages/liveblocks-python/liveblocks/api/management/send_test_webhook.py
new file mode 100644
index 0000000000..267adda370
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/send_test_webhook.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.test_management_webhook_request_body import TestManagementWebhookRequestBody
+from ...models.test_management_webhook_response import TestManagementWebhookResponse
+
+
+def _get_kwargs(
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: TestManagementWebhookRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}/webhooks/{webhook_id}/test".format(
+ project_id=quote(str(project_id), safe=""),
+ webhook_id=quote(str(webhook_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> TestManagementWebhookResponse:
+ if response.status_code == 200:
+ response_200 = TestManagementWebhookResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.Client,
+ body: TestManagementWebhookRequestBody,
+) -> TestManagementWebhookResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: TestManagementWebhookRequestBody,
+) -> TestManagementWebhookResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/update_management_project.py b/packages/liveblocks-python/liveblocks/api/management/update_management_project.py
new file mode 100644
index 0000000000..0ff82b71a5
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/update_management_project.py
@@ -0,0 +1,74 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.management_project import ManagementProject
+from ...models.update_management_project_request_body import UpdateManagementProjectRequestBody
+
+
+def _get_kwargs(
+ project_id: str,
+ *,
+ body: UpdateManagementProjectRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}".format(
+ project_id=quote(str(project_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> ManagementProject:
+ if response.status_code == 200:
+ response_200 = ManagementProject.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ *,
+ client: httpx.Client,
+ body: UpdateManagementProjectRequestBody,
+) -> ManagementProject:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UpdateManagementProjectRequestBody,
+) -> ManagementProject:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/update_management_webhook.py b/packages/liveblocks-python/liveblocks/api/management/update_management_webhook.py
new file mode 100644
index 0000000000..6e27adb1d8
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/update_management_webhook.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.management_webhook import ManagementWebhook
+from ...models.update_management_webhook_request_body import UpdateManagementWebhookRequestBody
+
+
+def _get_kwargs(
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: UpdateManagementWebhookRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}/webhooks/{webhook_id}".format(
+ project_id=quote(str(project_id), safe=""),
+ webhook_id=quote(str(webhook_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> ManagementWebhook:
+ if response.status_code == 200:
+ response_200 = ManagementWebhook.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.Client,
+ body: UpdateManagementWebhookRequestBody,
+) -> ManagementWebhook:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UpdateManagementWebhookRequestBody,
+) -> ManagementWebhook:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/management/upsert_management_webhook_additional_headers.py b/packages/liveblocks-python/liveblocks/api/management/upsert_management_webhook_additional_headers.py
new file mode 100644
index 0000000000..d57676d786
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/management/upsert_management_webhook_additional_headers.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.upsert_management_webhook_headers_request_body import UpsertManagementWebhookHeadersRequestBody
+from ...models.upsert_management_webhook_headers_response import UpsertManagementWebhookHeadersResponse
+
+
+def _get_kwargs(
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: UpsertManagementWebhookHeadersRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/management/projects/{project_id}/webhooks/{webhook_id}/additional-headers".format(
+ project_id=quote(str(project_id), safe=""),
+ webhook_id=quote(str(webhook_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> UpsertManagementWebhookHeadersResponse:
+ if response.status_code == 200:
+ response_200 = UpsertManagementWebhookHeadersResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.Client,
+ body: UpsertManagementWebhookHeadersRequestBody,
+) -> UpsertManagementWebhookHeadersResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ project_id: str,
+ webhook_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UpsertManagementWebhookHeadersRequestBody,
+) -> UpsertManagementWebhookHeadersResponse:
+ kwargs = _get_kwargs(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/__init__.py b/packages/liveblocks-python/liveblocks/api/notifications/__init__.py
new file mode 100644
index 0000000000..2d7c0b23da
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/delete_all_inbox_notifications.py b/packages/liveblocks-python/liveblocks/api/notifications/delete_all_inbox_notifications.py
new file mode 100644
index 0000000000..3ab7d95d62
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/delete_all_inbox_notifications.py
@@ -0,0 +1,58 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ user_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/users/{user_id}/inbox-notifications".format(
+ user_id=quote(str(user_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ user_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ user_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/delete_inbox_notification.py b/packages/liveblocks-python/liveblocks/api/notifications/delete_inbox_notification.py
new file mode 100644
index 0000000000..d3e40b6f0c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/delete_inbox_notification.py
@@ -0,0 +1,64 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ user_id: str,
+ inbox_notification_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/users/{user_id}/inbox-notifications/{inbox_notification_id}".format(
+ user_id=quote(str(user_id), safe=""),
+ inbox_notification_id=quote(str(inbox_notification_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ user_id: str,
+ inbox_notification_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ inbox_notification_id=inbox_notification_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ user_id: str,
+ inbox_notification_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ inbox_notification_id=inbox_notification_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/delete_notification_settings.py b/packages/liveblocks-python/liveblocks/api/notifications/delete_notification_settings.py
new file mode 100644
index 0000000000..4171f885ef
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/delete_notification_settings.py
@@ -0,0 +1,58 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ user_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/users/{user_id}/notification-settings".format(
+ user_id=quote(str(user_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ user_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ user_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/delete_room_notification_settings.py b/packages/liveblocks-python/liveblocks/api/notifications/delete_room_notification_settings.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/delete_room_subscription_settings.py b/packages/liveblocks-python/liveblocks/api/notifications/delete_room_subscription_settings.py
new file mode 100644
index 0000000000..894a4b590d
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/delete_room_subscription_settings.py
@@ -0,0 +1,64 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ room_id: str,
+ user_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/rooms/{room_id}/users/{user_id}/subscription-settings".format(
+ room_id=quote(str(room_id), safe=""),
+ user_id=quote(str(user_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ user_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ user_id=user_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ user_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ user_id=user_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/get_inbox_notification.py b/packages/liveblocks-python/liveblocks/api/notifications/get_inbox_notification.py
new file mode 100644
index 0000000000..cb8d7bcd0f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/get_inbox_notification.py
@@ -0,0 +1,84 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.inbox_notification_custom_data import InboxNotificationCustomData
+from ...models.inbox_notification_thread_data import InboxNotificationThreadData
+
+
+def _get_kwargs(
+ user_id: str,
+ inbox_notification_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/users/{user_id}/inbox-notifications/{inbox_notification_id}".format(
+ user_id=quote(str(user_id), safe=""),
+ inbox_notification_id=quote(str(inbox_notification_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> InboxNotificationCustomData | InboxNotificationThreadData:
+ if response.status_code == 200:
+
+ def _parse_response_200(data: object) -> InboxNotificationCustomData | InboxNotificationThreadData:
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ response_200_type_0 = InboxNotificationThreadData.from_dict(data)
+
+ return response_200_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ if not isinstance(data, dict):
+ raise TypeError()
+ response_200_type_1 = InboxNotificationCustomData.from_dict(data)
+
+ return response_200_type_1
+
+ response_200 = _parse_response_200(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ user_id: str,
+ inbox_notification_id: str,
+ *,
+ client: httpx.Client,
+) -> InboxNotificationCustomData | InboxNotificationThreadData:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ inbox_notification_id=inbox_notification_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ user_id: str,
+ inbox_notification_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> InboxNotificationCustomData | InboxNotificationThreadData:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ inbox_notification_id=inbox_notification_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/get_inbox_notifications.py b/packages/liveblocks-python/liveblocks/api/notifications/get_inbox_notifications.py
new file mode 100644
index 0000000000..c836b822fe
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/get_inbox_notifications.py
@@ -0,0 +1,96 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_inbox_notifications_response import GetInboxNotificationsResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ user_id: str,
+ *,
+ organization_id: str | Unset = UNSET,
+ query: str | Unset = UNSET,
+ limit: int | Unset = 50,
+ starting_after: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["organizationId"] = organization_id
+
+ params["query"] = query
+
+ params["limit"] = limit
+
+ params["startingAfter"] = starting_after
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/users/{user_id}/inbox-notifications".format(
+ user_id=quote(str(user_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetInboxNotificationsResponse:
+ if response.status_code == 200:
+ response_200 = GetInboxNotificationsResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ user_id: str,
+ *,
+ client: httpx.Client,
+ organization_id: str | Unset = UNSET,
+ query: str | Unset = UNSET,
+ limit: int | Unset = 50,
+ starting_after: str | Unset = UNSET,
+) -> GetInboxNotificationsResponse:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ organization_id=organization_id,
+ query=query,
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ user_id: str,
+ *,
+ client: httpx.AsyncClient,
+ organization_id: str | Unset = UNSET,
+ query: str | Unset = UNSET,
+ limit: int | Unset = 50,
+ starting_after: str | Unset = UNSET,
+) -> GetInboxNotificationsResponse:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ organization_id=organization_id,
+ query=query,
+ limit=limit,
+ starting_after=starting_after,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/get_notification_settings.py b/packages/liveblocks-python/liveblocks/api/notifications/get_notification_settings.py
new file mode 100644
index 0000000000..8731924602
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/get_notification_settings.py
@@ -0,0 +1,61 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.notification_settings import NotificationSettings
+
+
+def _get_kwargs(
+ user_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/users/{user_id}/notification-settings".format(
+ user_id=quote(str(user_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> NotificationSettings:
+ if response.status_code == 200:
+ response_200 = NotificationSettings.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ user_id: str,
+ *,
+ client: httpx.Client,
+) -> NotificationSettings:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ user_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> NotificationSettings:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/get_room_notification_settings.py b/packages/liveblocks-python/liveblocks/api/notifications/get_room_notification_settings.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/get_room_subscription_settings.py b/packages/liveblocks-python/liveblocks/api/notifications/get_room_subscription_settings.py
new file mode 100644
index 0000000000..4e038d8dae
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/get_room_subscription_settings.py
@@ -0,0 +1,67 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.room_subscription_settings import RoomSubscriptionSettings
+
+
+def _get_kwargs(
+ room_id: str,
+ user_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/users/{user_id}/subscription-settings".format(
+ room_id=quote(str(room_id), safe=""),
+ user_id=quote(str(user_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> RoomSubscriptionSettings:
+ if response.status_code == 200:
+ response_200 = RoomSubscriptionSettings.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ user_id: str,
+ *,
+ client: httpx.Client,
+) -> RoomSubscriptionSettings:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ user_id=user_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ user_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> RoomSubscriptionSettings:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ user_id=user_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/get_user_room_subscription_settings.py b/packages/liveblocks-python/liveblocks/api/notifications/get_user_room_subscription_settings.py
new file mode 100644
index 0000000000..4ca630107e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/get_user_room_subscription_settings.py
@@ -0,0 +1,89 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_room_subscription_settings_response import GetRoomSubscriptionSettingsResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ user_id: str,
+ *,
+ starting_after: str | Unset = UNSET,
+ limit: int | Unset = 50,
+ organization_id: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["startingAfter"] = starting_after
+
+ params["limit"] = limit
+
+ params["organizationId"] = organization_id
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/users/{user_id}/room-subscription-settings".format(
+ user_id=quote(str(user_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetRoomSubscriptionSettingsResponse:
+ if response.status_code == 200:
+ response_200 = GetRoomSubscriptionSettingsResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ user_id: str,
+ *,
+ client: httpx.Client,
+ starting_after: str | Unset = UNSET,
+ limit: int | Unset = 50,
+ organization_id: str | Unset = UNSET,
+) -> GetRoomSubscriptionSettingsResponse:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ starting_after=starting_after,
+ limit=limit,
+ organization_id=organization_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ user_id: str,
+ *,
+ client: httpx.AsyncClient,
+ starting_after: str | Unset = UNSET,
+ limit: int | Unset = 50,
+ organization_id: str | Unset = UNSET,
+) -> GetRoomSubscriptionSettingsResponse:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ starting_after=starting_after,
+ limit=limit,
+ organization_id=organization_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/trigger_inbox_notification.py b/packages/liveblocks-python/liveblocks/api/notifications/trigger_inbox_notification.py
new file mode 100644
index 0000000000..3fbc3acda0
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/trigger_inbox_notification.py
@@ -0,0 +1,65 @@
+from typing import Any
+
+import httpx
+
+from ... import errors
+from ...models.trigger_inbox_notification_request_body import TriggerInboxNotificationRequestBody
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ *,
+ body: TriggerInboxNotificationRequestBody | Unset = UNSET,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/inbox-notifications/trigger",
+ }
+
+ if not isinstance(body, Unset):
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 200:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ *,
+ client: httpx.Client,
+ body: TriggerInboxNotificationRequestBody | Unset = UNSET,
+) -> None:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ *,
+ client: httpx.AsyncClient,
+ body: TriggerInboxNotificationRequestBody | Unset = UNSET,
+) -> None:
+ kwargs = _get_kwargs(
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/update_notification_settings.py b/packages/liveblocks-python/liveblocks/api/notifications/update_notification_settings.py
new file mode 100644
index 0000000000..edf67efa07
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/update_notification_settings.py
@@ -0,0 +1,74 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.notification_settings import NotificationSettings
+from ...models.update_notification_settings_request_body import UpdateNotificationSettingsRequestBody
+
+
+def _get_kwargs(
+ user_id: str,
+ *,
+ body: UpdateNotificationSettingsRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/users/{user_id}/notification-settings".format(
+ user_id=quote(str(user_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> NotificationSettings:
+ if response.status_code == 200:
+ response_200 = NotificationSettings.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ user_id: str,
+ *,
+ client: httpx.Client,
+ body: UpdateNotificationSettingsRequestBody,
+) -> NotificationSettings:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ user_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UpdateNotificationSettingsRequestBody,
+) -> NotificationSettings:
+ kwargs = _get_kwargs(
+ user_id=user_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/update_room_notification_settings.py b/packages/liveblocks-python/liveblocks/api/notifications/update_room_notification_settings.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/liveblocks-python/liveblocks/api/notifications/update_room_subscription_settings.py b/packages/liveblocks-python/liveblocks/api/notifications/update_room_subscription_settings.py
new file mode 100644
index 0000000000..43d386fa22
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/notifications/update_room_subscription_settings.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.room_subscription_settings import RoomSubscriptionSettings
+from ...models.update_room_subscription_settings_request_body import UpdateRoomSubscriptionSettingsRequestBody
+
+
+def _get_kwargs(
+ room_id: str,
+ user_id: str,
+ *,
+ body: UpdateRoomSubscriptionSettingsRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/users/{user_id}/subscription-settings".format(
+ room_id=quote(str(room_id), safe=""),
+ user_id=quote(str(user_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> RoomSubscriptionSettings:
+ if response.status_code == 200:
+ response_200 = RoomSubscriptionSettings.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ user_id: str,
+ *,
+ client: httpx.Client,
+ body: UpdateRoomSubscriptionSettingsRequestBody,
+) -> RoomSubscriptionSettings:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ user_id=user_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ user_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UpdateRoomSubscriptionSettingsRequestBody,
+) -> RoomSubscriptionSettings:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ user_id=user_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/room/__init__.py b/packages/liveblocks-python/liveblocks/api/room/__init__.py
new file mode 100644
index 0000000000..2d7c0b23da
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
diff --git a/packages/liveblocks-python/liveblocks/api/room/broadcast_event.py b/packages/liveblocks-python/liveblocks/api/room/broadcast_event.py
new file mode 100644
index 0000000000..669381cf61
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/broadcast_event.py
@@ -0,0 +1,70 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ body: Any,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/broadcast_event".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ body: Any,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: Any,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/room/create_room.py b/packages/liveblocks-python/liveblocks/api/room/create_room.py
new file mode 100644
index 0000000000..f1ab1d1e24
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/create_room.py
@@ -0,0 +1,79 @@
+from typing import Any
+
+import httpx
+
+from ... import errors
+from ...models.create_room_request_body import CreateRoomRequestBody
+from ...models.room import Room
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ *,
+ body: CreateRoomRequestBody,
+ idempotent: bool | Unset = UNSET,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ params: dict[str, Any] = {}
+
+ params["idempotent"] = idempotent
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms",
+ "params": params,
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Room:
+ if response.status_code == 200:
+ response_200 = Room.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ *,
+ client: httpx.Client,
+ body: CreateRoomRequestBody,
+ idempotent: bool | Unset = UNSET,
+) -> Room:
+ kwargs = _get_kwargs(
+ body=body,
+ idempotent=idempotent,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ *,
+ client: httpx.AsyncClient,
+ body: CreateRoomRequestBody,
+ idempotent: bool | Unset = UNSET,
+) -> Room:
+ kwargs = _get_kwargs(
+ body=body,
+ idempotent=idempotent,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/room/delete_room.py b/packages/liveblocks-python/liveblocks/api/room/delete_room.py
new file mode 100644
index 0000000000..52e1d126af
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/delete_room.py
@@ -0,0 +1,58 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ room_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/rooms/{room_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/room/get_active_users.py b/packages/liveblocks-python/liveblocks/api/room/get_active_users.py
new file mode 100644
index 0000000000..17f7c71091
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/get_active_users.py
@@ -0,0 +1,61 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.active_users_response import ActiveUsersResponse
+
+
+def _get_kwargs(
+ room_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/active_users".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> ActiveUsersResponse:
+ if response.status_code == 200:
+ response_200 = ActiveUsersResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+) -> ActiveUsersResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> ActiveUsersResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/room/get_room.py b/packages/liveblocks-python/liveblocks/api/room/get_room.py
new file mode 100644
index 0000000000..2d5ec06fdc
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/get_room.py
@@ -0,0 +1,61 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.room import Room
+
+
+def _get_kwargs(
+ room_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Room:
+ if response.status_code == 200:
+ response_200 = Room.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+) -> Room:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> Room:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/room/get_rooms.py b/packages/liveblocks-python/liveblocks/api/room/get_rooms.py
new file mode 100644
index 0000000000..5628f20293
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/get_rooms.py
@@ -0,0 +1,102 @@
+from typing import Any
+
+import httpx
+
+from ... import errors
+from ...models.get_rooms_response import GetRoomsResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ organization_id: str | Unset = UNSET,
+ query: str | Unset = UNSET,
+ user_id: str | Unset = UNSET,
+ group_ids: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["limit"] = limit
+
+ params["startingAfter"] = starting_after
+
+ params["organizationId"] = organization_id
+
+ params["query"] = query
+
+ params["userId"] = user_id
+
+ params["groupIds"] = group_ids
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms",
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetRoomsResponse:
+ if response.status_code == 200:
+ response_200 = GetRoomsResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ *,
+ client: httpx.Client,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ organization_id: str | Unset = UNSET,
+ query: str | Unset = UNSET,
+ user_id: str | Unset = UNSET,
+ group_ids: str | Unset = UNSET,
+) -> GetRoomsResponse:
+ kwargs = _get_kwargs(
+ limit=limit,
+ starting_after=starting_after,
+ organization_id=organization_id,
+ query=query,
+ user_id=user_id,
+ group_ids=group_ids,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ *,
+ client: httpx.AsyncClient,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ organization_id: str | Unset = UNSET,
+ query: str | Unset = UNSET,
+ user_id: str | Unset = UNSET,
+ group_ids: str | Unset = UNSET,
+) -> GetRoomsResponse:
+ kwargs = _get_kwargs(
+ limit=limit,
+ starting_after=starting_after,
+ organization_id=organization_id,
+ query=query,
+ user_id=user_id,
+ group_ids=group_ids,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/room/prewarm_room.py b/packages/liveblocks-python/liveblocks/api/room/prewarm_room.py
new file mode 100644
index 0000000000..b17cc87d66
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/prewarm_room.py
@@ -0,0 +1,58 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ room_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/prewarm".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/room/set_presence.py b/packages/liveblocks-python/liveblocks/api/room/set_presence.py
new file mode 100644
index 0000000000..f8c17b852b
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/set_presence.py
@@ -0,0 +1,71 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.set_presence_request_body import SetPresenceRequestBody
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ body: SetPresenceRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/presence".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ body: SetPresenceRequestBody,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: SetPresenceRequestBody,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/room/update_room.py b/packages/liveblocks-python/liveblocks/api/room/update_room.py
new file mode 100644
index 0000000000..8395eaa043
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/update_room.py
@@ -0,0 +1,74 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.room import Room
+from ...models.update_room_request_body import UpdateRoomRequestBody
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ body: UpdateRoomRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Room:
+ if response.status_code == 200:
+ response_200 = Room.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ body: UpdateRoomRequestBody,
+) -> Room:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UpdateRoomRequestBody,
+) -> Room:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/room/update_room_id.py b/packages/liveblocks-python/liveblocks/api/room/update_room_id.py
new file mode 100644
index 0000000000..4d2846c52a
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/update_room_id.py
@@ -0,0 +1,76 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.room import Room
+from ...models.update_room_id_request_body import UpdateRoomIdRequestBody
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ body: UpdateRoomIdRequestBody | Unset = UNSET,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/update-room-id".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ if not isinstance(body, Unset):
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Room:
+ if response.status_code == 200:
+ response_200 = Room.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ body: UpdateRoomIdRequestBody | Unset = UNSET,
+) -> Room:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UpdateRoomIdRequestBody | Unset = UNSET,
+) -> Room:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/room/upsert_room.py b/packages/liveblocks-python/liveblocks/api/room/upsert_room.py
new file mode 100644
index 0000000000..af3c6aa3cb
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/room/upsert_room.py
@@ -0,0 +1,74 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.room import Room
+from ...models.upsert_room_request_body import UpsertRoomRequestBody
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ body: UpsertRoomRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/upsert".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Room:
+ if response.status_code == 200:
+ response_200 = Room.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ body: UpsertRoomRequestBody,
+) -> Room:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UpsertRoomRequestBody,
+) -> Room:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/storage/__init__.py b/packages/liveblocks-python/liveblocks/api/storage/__init__.py
new file mode 100644
index 0000000000..2d7c0b23da
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/storage/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
diff --git a/packages/liveblocks-python/liveblocks/api/storage/delete_storage_document.py b/packages/liveblocks-python/liveblocks/api/storage/delete_storage_document.py
new file mode 100644
index 0000000000..4e5d6b830e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/storage/delete_storage_document.py
@@ -0,0 +1,58 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ room_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/rooms/{room_id}/storage".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 200:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/storage/get_storage_document.py b/packages/liveblocks-python/liveblocks/api/storage/get_storage_document.py
new file mode 100644
index 0000000000..cd60c2b45e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/storage/get_storage_document.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_storage_document_format import GetStorageDocumentFormat
+from ...models.get_storage_document_response import GetStorageDocumentResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ format_: GetStorageDocumentFormat | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ json_format_: str | Unset = UNSET
+ if not isinstance(format_, Unset):
+ json_format_ = format_.value
+
+ params["format"] = json_format_
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/storage".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetStorageDocumentResponse:
+ if response.status_code == 200:
+ response_200 = GetStorageDocumentResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ format_: GetStorageDocumentFormat | Unset = UNSET,
+) -> GetStorageDocumentResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ format_=format_,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ format_: GetStorageDocumentFormat | Unset = UNSET,
+) -> GetStorageDocumentResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ format_=format_,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/storage/initialize_storage_document.py b/packages/liveblocks-python/liveblocks/api/storage/initialize_storage_document.py
new file mode 100644
index 0000000000..bb660cb7e4
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/storage/initialize_storage_document.py
@@ -0,0 +1,76 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.initialize_storage_document_body import InitializeStorageDocumentBody
+from ...models.initialize_storage_document_response import InitializeStorageDocumentResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ body: InitializeStorageDocumentBody | Unset = UNSET,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/storage".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ if not isinstance(body, Unset):
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> InitializeStorageDocumentResponse:
+ if response.status_code == 200:
+ response_200 = InitializeStorageDocumentResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ body: InitializeStorageDocumentBody | Unset = UNSET,
+) -> InitializeStorageDocumentResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: InitializeStorageDocumentBody | Unset = UNSET,
+) -> InitializeStorageDocumentResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/storage/patch_storage_document.py b/packages/liveblocks-python/liveblocks/api/storage/patch_storage_document.py
new file mode 100644
index 0000000000..2c80efbc44
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/storage/patch_storage_document.py
@@ -0,0 +1,125 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.add_json_patch_operation import AddJsonPatchOperation
+from ...models.copy_json_patch_operation import CopyJsonPatchOperation
+from ...models.move_json_patch_operation import MoveJsonPatchOperation
+from ...models.remove_json_patch_operation import RemoveJsonPatchOperation
+from ...models.replace_json_patch_operation import ReplaceJsonPatchOperation
+from ...models.test_json_patch_operation import TestJsonPatchOperation
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ body: list[
+ AddJsonPatchOperation
+ | CopyJsonPatchOperation
+ | MoveJsonPatchOperation
+ | RemoveJsonPatchOperation
+ | ReplaceJsonPatchOperation
+ | TestJsonPatchOperation
+ ],
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "patch",
+ "url": "/v2/rooms/{room_id}/storage/json-patch".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = []
+ for componentsschemas_patch_storage_document_request_body_item_data in body:
+ componentsschemas_patch_storage_document_request_body_item: dict[str, Any]
+ if isinstance(componentsschemas_patch_storage_document_request_body_item_data, AddJsonPatchOperation):
+ componentsschemas_patch_storage_document_request_body_item = (
+ componentsschemas_patch_storage_document_request_body_item_data.to_dict()
+ )
+ elif isinstance(componentsschemas_patch_storage_document_request_body_item_data, RemoveJsonPatchOperation):
+ componentsschemas_patch_storage_document_request_body_item = (
+ componentsschemas_patch_storage_document_request_body_item_data.to_dict()
+ )
+ elif isinstance(componentsschemas_patch_storage_document_request_body_item_data, ReplaceJsonPatchOperation):
+ componentsschemas_patch_storage_document_request_body_item = (
+ componentsschemas_patch_storage_document_request_body_item_data.to_dict()
+ )
+ elif isinstance(componentsschemas_patch_storage_document_request_body_item_data, CopyJsonPatchOperation):
+ componentsschemas_patch_storage_document_request_body_item = (
+ componentsschemas_patch_storage_document_request_body_item_data.to_dict()
+ )
+ elif isinstance(componentsschemas_patch_storage_document_request_body_item_data, MoveJsonPatchOperation):
+ componentsschemas_patch_storage_document_request_body_item = (
+ componentsschemas_patch_storage_document_request_body_item_data.to_dict()
+ )
+ else:
+ componentsschemas_patch_storage_document_request_body_item = (
+ componentsschemas_patch_storage_document_request_body_item_data.to_dict()
+ )
+
+ _kwargs["json"].append(componentsschemas_patch_storage_document_request_body_item)
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 200:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ body: list[
+ AddJsonPatchOperation
+ | CopyJsonPatchOperation
+ | MoveJsonPatchOperation
+ | RemoveJsonPatchOperation
+ | ReplaceJsonPatchOperation
+ | TestJsonPatchOperation
+ ],
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: list[
+ AddJsonPatchOperation
+ | CopyJsonPatchOperation
+ | MoveJsonPatchOperation
+ | RemoveJsonPatchOperation
+ | ReplaceJsonPatchOperation
+ | TestJsonPatchOperation
+ ],
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/yjs/__init__.py b/packages/liveblocks-python/liveblocks/api/yjs/__init__.py
new file mode 100644
index 0000000000..2d7c0b23da
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/yjs/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
diff --git a/packages/liveblocks-python/liveblocks/api/yjs/create_yjs_version.py b/packages/liveblocks-python/liveblocks/api/yjs/create_yjs_version.py
new file mode 100644
index 0000000000..d90ad29953
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/yjs/create_yjs_version.py
@@ -0,0 +1,61 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.create_yjs_version_response import CreateYjsVersionResponse
+
+
+def _get_kwargs(
+ room_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/version".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> CreateYjsVersionResponse:
+ if response.status_code == 200:
+ response_200 = CreateYjsVersionResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+) -> CreateYjsVersionResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> CreateYjsVersionResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_document.py b/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_document.py
new file mode 100644
index 0000000000..2a5e4c3a88
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_document.py
@@ -0,0 +1,94 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_yjs_document_response import GetYjsDocumentResponse
+from ...models.get_yjs_document_type import GetYjsDocumentType
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ formatting: bool | Unset = UNSET,
+ key: str | Unset = UNSET,
+ type_: GetYjsDocumentType | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["formatting"] = formatting
+
+ params["key"] = key
+
+ json_type_: str | Unset = UNSET
+ if not isinstance(type_, Unset):
+ json_type_ = type_.value
+
+ params["type"] = json_type_
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/ydoc".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetYjsDocumentResponse:
+ if response.status_code == 200:
+ response_200 = GetYjsDocumentResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ formatting: bool | Unset = UNSET,
+ key: str | Unset = UNSET,
+ type_: GetYjsDocumentType | Unset = UNSET,
+) -> GetYjsDocumentResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ formatting=formatting,
+ key=key,
+ type_=type_,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ formatting: bool | Unset = UNSET,
+ key: str | Unset = UNSET,
+ type_: GetYjsDocumentType | Unset = UNSET,
+) -> GetYjsDocumentResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ formatting=formatting,
+ key=key,
+ type_=type_,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_document_as_binary_update.py b/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_document_as_binary_update.py
new file mode 100644
index 0000000000..a7eee2da7a
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_document_as_binary_update.py
@@ -0,0 +1,75 @@
+from io import BytesIO
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...types import UNSET, File, Unset
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ guid: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["guid"] = guid
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/ydoc-binary".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> File:
+ if response.status_code == 200:
+ response_200 = File(payload=BytesIO(response.content))
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ guid: str | Unset = UNSET,
+) -> File:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ guid=guid,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ guid: str | Unset = UNSET,
+) -> File:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ guid=guid,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_version.py b/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_version.py
new file mode 100644
index 0000000000..089b6b6a59
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_version.py
@@ -0,0 +1,68 @@
+from io import BytesIO
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...types import File
+
+
+def _get_kwargs(
+ room_id: str,
+ version_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/version/{version_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ version_id=quote(str(version_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> File:
+ if response.status_code == 200:
+ response_200 = File(payload=BytesIO(response.content))
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ version_id: str,
+ *,
+ client: httpx.Client,
+) -> File:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ version_id=version_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ version_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> File:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ version_id=version_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_versions.py b/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_versions.py
new file mode 100644
index 0000000000..f6301771d6
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_versions.py
@@ -0,0 +1,82 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_yjs_versions_response import GetYjsVersionsResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["limit"] = limit
+
+ params["cursor"] = cursor
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/versions".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetYjsVersionsResponse:
+ if response.status_code == 200:
+ response_200 = GetYjsVersionsResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+) -> GetYjsVersionsResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ limit=limit,
+ cursor=cursor,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+) -> GetYjsVersionsResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ limit=limit,
+ cursor=cursor,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/yjs/send_yjs_binary_update.py b/packages/liveblocks-python/liveblocks/api/yjs/send_yjs_binary_update.py
new file mode 100644
index 0000000000..b39a1b50f3
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/yjs/send_yjs_binary_update.py
@@ -0,0 +1,83 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...types import UNSET, File, Unset
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ body: File,
+ guid: str | Unset = UNSET,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ params: dict[str, Any] = {}
+
+ params["guid"] = guid
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "put",
+ "url": "/v2/rooms/{room_id}/ydoc".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ _kwargs["content"] = body.payload
+
+ headers["Content-Type"] = "application/octet-stream"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 200:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ body: File,
+ guid: str | Unset = UNSET,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ guid=guid,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: File,
+ guid: str | Unset = UNSET,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ guid=guid,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/client.py b/packages/liveblocks-python/liveblocks/client.py
new file mode 100644
index 0000000000..5eabf18e95
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/client.py
@@ -0,0 +1,6822 @@
+from __future__ import annotations
+
+import re
+from typing import TYPE_CHECKING, Any
+
+import httpx
+
+from .types import UNSET, File, Unset
+
+if TYPE_CHECKING:
+ from .models.active_users_response import ActiveUsersResponse
+ from .models.add_comment_reaction_request_body import AddCommentReactionRequestBody
+ from .models.add_group_members_request_body import AddGroupMembersRequestBody
+ from .models.add_json_patch_operation import AddJsonPatchOperation
+ from .models.ai_copilot_anthropic import AiCopilotAnthropic
+ from .models.ai_copilot_google import AiCopilotGoogle
+ from .models.ai_copilot_open_ai import AiCopilotOpenAi
+ from .models.ai_copilot_open_ai_compatible import AiCopilotOpenAiCompatible
+ from .models.authorize_user_request_body import AuthorizeUserRequestBody
+ from .models.authorize_user_response import AuthorizeUserResponse
+ from .models.comment import Comment
+ from .models.comment_metadata import CommentMetadata
+ from .models.comment_reaction import CommentReaction
+ from .models.copy_json_patch_operation import CopyJsonPatchOperation
+ from .models.create_ai_copilot_options_anthropic import CreateAiCopilotOptionsAnthropic
+ from .models.create_ai_copilot_options_google import CreateAiCopilotOptionsGoogle
+ from .models.create_ai_copilot_options_open_ai import CreateAiCopilotOptionsOpenAi
+ from .models.create_ai_copilot_options_open_ai_compatible import CreateAiCopilotOptionsOpenAiCompatible
+ from .models.create_comment_request_body import CreateCommentRequestBody
+ from .models.create_file_knowledge_source_response_200 import CreateFileKnowledgeSourceResponse200
+ from .models.create_group_request_body import CreateGroupRequestBody
+ from .models.create_management_project_request_body import CreateManagementProjectRequestBody
+ from .models.create_management_webhook_request_body import CreateManagementWebhookRequestBody
+ from .models.create_room_request_body import CreateRoomRequestBody
+ from .models.create_thread_request_body import CreateThreadRequestBody
+ from .models.create_web_knowledge_source_request_body import CreateWebKnowledgeSourceRequestBody
+ from .models.create_web_knowledge_source_response import CreateWebKnowledgeSourceResponse
+ from .models.create_yjs_version_response import CreateYjsVersionResponse
+ from .models.delete_management_webhook_headers_request_body import DeleteManagementWebhookHeadersRequestBody
+ from .models.delete_management_webhook_headers_response import DeleteManagementWebhookHeadersResponse
+ from .models.edit_comment_metadata_request_body import EditCommentMetadataRequestBody
+ from .models.edit_comment_request_body import EditCommentRequestBody
+ from .models.edit_thread_metadata_request_body import EditThreadMetadataRequestBody
+ from .models.get_ai_copilots_response import GetAiCopilotsResponse
+ from .models.get_file_knowledge_source_markdown_response import GetFileKnowledgeSourceMarkdownResponse
+ from .models.get_groups_response import GetGroupsResponse
+ from .models.get_inbox_notifications_response import GetInboxNotificationsResponse
+ from .models.get_knowledge_sources_response import GetKnowledgeSourcesResponse
+ from .models.get_management_projects_response import GetManagementProjectsResponse
+ from .models.get_management_webhook_headers_response import GetManagementWebhookHeadersResponse
+ from .models.get_management_webhooks_response import GetManagementWebhooksResponse
+ from .models.get_room_subscription_settings_response import GetRoomSubscriptionSettingsResponse
+ from .models.get_rooms_response import GetRoomsResponse
+ from .models.get_storage_document_format import GetStorageDocumentFormat
+ from .models.get_storage_document_response import GetStorageDocumentResponse
+ from .models.get_thread_subscriptions_response import GetThreadSubscriptionsResponse
+ from .models.get_threads_response import GetThreadsResponse
+ from .models.get_user_groups_response import GetUserGroupsResponse
+ from .models.get_web_knowledge_source_links_response import GetWebKnowledgeSourceLinksResponse
+ from .models.get_yjs_document_response import GetYjsDocumentResponse
+ from .models.get_yjs_document_type import GetYjsDocumentType
+ from .models.get_yjs_versions_response import GetYjsVersionsResponse
+ from .models.group import Group
+ from .models.identify_user_request_body import IdentifyUserRequestBody
+ from .models.identify_user_response import IdentifyUserResponse
+ from .models.inbox_notification_custom_data import InboxNotificationCustomData
+ from .models.inbox_notification_thread_data import InboxNotificationThreadData
+ from .models.initialize_storage_document_body import InitializeStorageDocumentBody
+ from .models.initialize_storage_document_response import InitializeStorageDocumentResponse
+ from .models.knowledge_source_file_source import KnowledgeSourceFileSource
+ from .models.knowledge_source_web_source import KnowledgeSourceWebSource
+ from .models.management_project import ManagementProject
+ from .models.management_project_roll_project_secret_api_key_response_secret_key_response import (
+ ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse,
+ )
+ from .models.management_webhook import ManagementWebhook
+ from .models.mark_thread_as_resolved_request_body import MarkThreadAsResolvedRequestBody
+ from .models.mark_thread_as_unresolved_request_body import MarkThreadAsUnresolvedRequestBody
+ from .models.move_json_patch_operation import MoveJsonPatchOperation
+ from .models.notification_settings import NotificationSettings
+ from .models.recover_management_webhook_failed_messages_request_body import (
+ RecoverManagementWebhookFailedMessagesRequestBody,
+ )
+ from .models.remove_comment_reaction_request_body import RemoveCommentReactionRequestBody
+ from .models.remove_group_members_request_body import RemoveGroupMembersRequestBody
+ from .models.remove_json_patch_operation import RemoveJsonPatchOperation
+ from .models.replace_json_patch_operation import ReplaceJsonPatchOperation
+ from .models.roll_project_public_api_key_request_body import RollProjectPublicApiKeyRequestBody
+ from .models.roll_project_public_api_key_response import RollProjectPublicApiKeyResponse
+ from .models.roll_project_secret_api_key_request_body import RollProjectSecretApiKeyRequestBody
+ from .models.room import Room
+ from .models.room_subscription_settings import RoomSubscriptionSettings
+ from .models.rotate_management_webhook_secret_response import RotateManagementWebhookSecretResponse
+ from .models.set_presence_request_body import SetPresenceRequestBody
+ from .models.subscribe_to_thread_request_body import SubscribeToThreadRequestBody
+ from .models.subscription import Subscription
+ from .models.test_json_patch_operation import TestJsonPatchOperation
+ from .models.test_management_webhook_request_body import TestManagementWebhookRequestBody
+ from .models.test_management_webhook_response import TestManagementWebhookResponse
+ from .models.thread import Thread
+ from .models.thread_metadata import ThreadMetadata
+ from .models.trigger_inbox_notification_request_body import TriggerInboxNotificationRequestBody
+ from .models.unsubscribe_from_thread_request_body import UnsubscribeFromThreadRequestBody
+ from .models.update_ai_copilot_request_body import UpdateAiCopilotRequestBody
+ from .models.update_management_project_request_body import UpdateManagementProjectRequestBody
+ from .models.update_management_webhook_request_body import UpdateManagementWebhookRequestBody
+ from .models.update_notification_settings_request_body import UpdateNotificationSettingsRequestBody
+ from .models.update_room_id_request_body import UpdateRoomIdRequestBody
+ from .models.update_room_request_body import UpdateRoomRequestBody
+ from .models.update_room_subscription_settings_request_body import UpdateRoomSubscriptionSettingsRequestBody
+ from .models.upsert_management_webhook_headers_request_body import UpsertManagementWebhookHeadersRequestBody
+ from .models.upsert_management_webhook_headers_response import UpsertManagementWebhookHeadersResponse
+ from .models.upsert_room_request_body import UpsertRoomRequestBody
+ from .session import AsyncSession, Session
+
+_DEFAULT_BASE_URL = "https://api.liveblocks.io"
+_VALID_KEY_CHARS_REGEX = re.compile(r"^[\w-]+$")
+
+
+def _assert_secret_key(value: str) -> None:
+ if not value.startswith("sk_"):
+ raise ValueError(
+ "Invalid value for 'secret'. Secret keys must start with 'sk_'. "
+ "Please provide the secret key from your Liveblocks dashboard at "
+ "https://liveblocks.io/dashboard/apikeys."
+ )
+ if not _VALID_KEY_CHARS_REGEX.match(value):
+ raise ValueError(
+ "Invalid chars found in 'secret'. Please check that you correctly "
+ "copied the secret key from your Liveblocks dashboard at "
+ "https://liveblocks.io/dashboard/apikeys."
+ )
+
+
+class Liveblocks:
+ """Synchronous client for the Liveblocks API.
+
+ Args:
+ secret: The Liveblocks secret key. Must start with ``sk_``.
+ Get it from https://liveblocks.io/dashboard/apikeys
+ base_url: Point the client to an alternative Liveblocks server.
+ """
+
+ _client: httpx.Client
+
+ def __init__(self, *, secret: str, base_url: str | None = None) -> None:
+ _assert_secret_key(secret)
+ self._client = httpx.Client(
+ base_url=base_url or _DEFAULT_BASE_URL,
+ headers={"Authorization": f"Bearer {secret}"},
+ )
+
+ def close(self) -> None:
+ self._client.close()
+
+ def __enter__(self) -> Liveblocks:
+ self._client.__enter__()
+ return self
+
+ def __exit__(self, *args: Any, **kwargs: Any) -> None:
+ self._client.__exit__(*args, **kwargs)
+
+ def prepare_session(
+ self,
+ user_id: str,
+ user_info: dict[str, Any] | None = None,
+ organization_id: str | None = None,
+ ) -> Session:
+ from .session import Session
+
+ return Session(
+ client=self,
+ user_id=user_id,
+ user_info=user_info,
+ organization_id=organization_id,
+ )
+
+ def get_rooms(
+ self,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ organization_id: str | Unset = UNSET,
+ query: str | Unset = UNSET,
+ user_id: str | Unset = UNSET,
+ group_ids: str | Unset = UNSET,
+ ) -> GetRoomsResponse:
+ """Get rooms
+
+ This endpoint returns a list of your rooms. The rooms are returned sorted by creation date, from
+ newest to oldest. You can filter rooms by room ID prefixes, metadata, users accesses, and groups
+ accesses. Corresponds to [`liveblocks.getRooms`](https://liveblocks.io/docs/api-
+ reference/liveblocks-node#get-rooms).
+
+ There is a pagination system where the cursor to the next page is returned in the response as
+ `nextCursor`, which can be combined with `startingAfter`.
+ You can also limit the number of rooms by query.
+
+ Filtering by metadata works by giving key values like `metadata.color=red`. Of course you can
+ combine multiple metadata clauses to refine the response like
+ `metadata.color=red&metadata.type=text`. Notice here the operator AND is applied between each
+ clauses.
+
+ Filtering by groups or userId works by giving a list of groups like
+ `groupIds=marketing,GZo7tQ,product` or/and a userId like `userId=user1`.
+ Notice here the operator OR is applied between each `groupIds` and the `userId`.
+
+ Args:
+ limit (int | Unset): A limit on the number of rooms to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+ organization_id (str | Unset): A filter on organization ID. Example: org_123456789.
+ query (str | Unset): Query to filter rooms. You can filter by `roomId` and `metadata`, for
+ example, `metadata["roomType"]:"whiteboard" AND roomId^"liveblocks:engineering"`. Learn
+ more about [filtering rooms with query language](https://liveblocks.io/docs/guides/how-to-
+ filter-rooms-using-query-language). Example: metadata["color"]:"blue".
+ user_id (str | Unset): A filter on users accesses. Example: user-123.
+ group_ids (str | Unset): A filter on groups accesses. Multiple groups can be used.
+ Example: group1,group2.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetRoomsResponse
+ """
+
+ from .api.room import get_rooms
+
+ return get_rooms._sync(
+ limit=limit,
+ starting_after=starting_after,
+ organization_id=organization_id,
+ query=query,
+ user_id=user_id,
+ group_ids=group_ids,
+ client=self._client,
+ )
+
+ def create_room(
+ self,
+ *,
+ body: CreateRoomRequestBody,
+ idempotent: bool | Unset = UNSET,
+ ) -> Room:
+ r"""Create room
+
+ This endpoint creates a new room. `id` and `defaultAccesses` are required. When provided with a
+ `?idempotent` query argument, will not return a 409 when the room already exists, but instead return
+ the existing room as-is. Corresponds to [`liveblocks.createRoom`](https://liveblocks.io/docs/api-
+ reference/liveblocks-node#post-rooms), or to
+ [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-
+ create-rooms-roomId) when `?idempotent` is provided.
+ - `defaultAccesses` could be `[]` or `[\"room:write\"]` (private or public).
+ - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries.
+ Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum.
+ `metadata` is optional field.
+ - `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain
+ 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+ - `groupsAccesses` are optional fields.
+
+ Args:
+ idempotent (bool | Unset): When provided, will not return a 409 when the room already
+ exists, but instead return the existing room as-is. Corresponds to
+ [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-or-create-rooms-roomId). Example: True.
+ body (CreateRoomRequestBody): Example: {'id': 'my-room-id', 'defaultAccesses':
+ ['room:write'], 'metadata': {'color': 'blue'}, 'usersAccesses': {'alice': ['room:write']},
+ 'groupsAccesses': {'product': ['room:write']}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Room
+ """
+
+ from .api.room import create_room
+
+ return create_room._sync(
+ body=body,
+ idempotent=idempotent,
+ client=self._client,
+ )
+
+ def get_room(
+ self,
+ room_id: str,
+ ) -> Room:
+ """Get room
+
+ This endpoint returns a room by its ID. Corresponds to
+ [`liveblocks.getRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomid).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Room
+ """
+
+ from .api.room import get_room
+
+ return get_room._sync(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ def update_room(
+ self,
+ room_id: str,
+ *,
+ body: UpdateRoomRequestBody,
+ ) -> Room:
+ r"""Update room
+
+ This endpoint updates specific properties of a room. Corresponds to
+ [`liveblocks.updateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomid).
+
+ It’s not necessary to provide the entire room’s information.
+ Setting a property to `null` means to delete this property. For example, if you want to remove
+ access to a specific user without losing other users:
+ ``{
+ \"usersAccesses\": {
+ \"john\": null
+ }
+ }``
+ `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.
+
+ - `defaultAccesses` could be `[]` or `[\"room:write\"]` (private or public).
+ - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries.
+ Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum.
+ `metadata` is optional field.
+ - `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain
+ 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+ - `groupsAccesses` could be `[]` or `[\"room:write\"]` for every records. `groupsAccesses` can
+ contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional
+ field.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (UpdateRoomRequestBody): Example: {'defaultAccesses': ['room:write'],
+ 'usersAccesses': {'alice': ['room:write']}, 'groupsAccesses': {'marketing':
+ ['room:write']}, 'metadata': {'color': 'blue'}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Room
+ """
+
+ from .api.room import update_room
+
+ return update_room._sync(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ def delete_room(
+ self,
+ room_id: str,
+ ) -> None:
+ """Delete room
+
+ This endpoint deletes a room. A deleted room is no longer accessible from the API or the dashboard
+ and it cannot be restored. Corresponds to [`liveblocks.deleteRoom`](https://liveblocks.io/docs/api-
+ reference/liveblocks-node#delete-rooms-roomid).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.room import delete_room
+
+ return delete_room._sync(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ def prewarm_room(
+ self,
+ room_id: str,
+ ) -> None:
+ """Prewarm room
+
+ Speeds up connecting to a room for the next 10 seconds. Use this when you know a user will be
+ connecting to a room with [`RoomProvider`](https://liveblocks.io/docs/api-reference/liveblocks-
+ react#RoomProvider) or [`enterRoom`](https://liveblocks.io/docs/api-reference/liveblocks-
+ client#Client.enterRoom) within 10 seconds, and the room will load quicker. Corresponds to
+ [`liveblocks.prewarmRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-
+ roomid-prewarm).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.room import prewarm_room
+
+ return prewarm_room._sync(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ def upsert_room(
+ self,
+ room_id: str,
+ *,
+ body: UpsertRoomRequestBody,
+ ) -> Room:
+ r"""Upsert (update or create) room
+
+ This endpoint updates specific properties of a room. Corresponds to
+ [`liveblocks.upsertRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#upsert-rooms-
+ roomId).
+
+ It’s not necessary to provide the entire room’s information.
+ Setting a property to `null` means to delete this property. For example, if you want to remove
+ access to a specific user without losing other users:
+ ``{
+ \"usersAccesses\": {
+ \"john\": null
+ }
+ }``
+ `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.
+
+ - `defaultAccesses` could be `[]` or `[\"room:write\"]` (private or public).
+ - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries.
+ Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum.
+ `metadata` is optional field.
+ - `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain
+ 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+ - `groupsAccesses` could be `[]` or `[\"room:write\"]` for every records. `groupsAccesses` can
+ contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional
+ field.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (UpsertRoomRequestBody): Example: {'update': {'usersAccesses': {'alice':
+ ['room:write']}, 'groupsAccesses': {'marketing': ['room:write']}, 'metadata': {'color':
+ 'blue'}}, 'create': {'defaultAccesses': ['room:write']}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Room
+ """
+
+ from .api.room import upsert_room
+
+ return upsert_room._sync(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ def update_room_id(
+ self,
+ room_id: str,
+ *,
+ body: UpdateRoomIdRequestBody | Unset = UNSET,
+ ) -> Room:
+ """Update room ID
+
+ This endpoint permanently updates the room’s ID. All existing references to the old room ID will
+ need to be updated. Returns the updated room. Corresponds to
+ [`liveblocks.updateRoomId`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomid-update-room-id).
+
+ Args:
+ room_id (str): The new ID for the room Example: my-room-id.
+ body (UpdateRoomIdRequestBody | Unset): Example: {'newRoomId': 'new-room-id'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Room
+ """
+
+ from .api.room import update_room_id
+
+ return update_room_id._sync(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ def get_active_users(
+ self,
+ room_id: str,
+ ) -> ActiveUsersResponse:
+ """Get active users
+
+ This endpoint returns a list of users currently present in the requested room. Corresponds to
+ [`liveblocks.getActiveUsers`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-
+ roomid-active-users).
+
+ For optimal performance, we recommend calling this endpoint no more than once every 10 seconds.
+ Duplicates can occur if a user is in the requested room with multiple browser tabs opened.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ActiveUsersResponse
+ """
+
+ from .api.room import get_active_users
+
+ return get_active_users._sync(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ def set_presence(
+ self,
+ room_id: str,
+ *,
+ body: SetPresenceRequestBody,
+ ) -> None:
+ """Set ephemeral presence
+
+ This endpoint sets ephemeral presence for a user in a room without requiring a WebSocket connection.
+ The presence data will automatically expire after the specified TTL (time-to-live). This is useful
+ for scenarios like showing an AI agent's presence in a room. The presence will be broadcast to all
+ connected users in the room. Corresponds to
+ [`liveblocks.setPresence`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-presence).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (SetPresenceRequestBody): Example: {'userId': 'agent-123', 'data': {'status':
+ 'active', 'cursor': {'x': 100, 'y': 200}}, 'userInfo': {'name': 'AI Assistant', 'avatar':
+ 'https://example.org/images/agent123.jpg'}, 'ttl': 60}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.room import set_presence
+
+ return set_presence._sync(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ def broadcast_event(
+ self,
+ room_id: str,
+ *,
+ body: Any,
+ ) -> None:
+ """Broadcast event to a room
+
+ This endpoint enables the broadcast of an event to a room without having to connect to it via the
+ `client` from `@liveblocks/client`. It takes any valid JSON as a request body. The `connectionId`
+ passed to event listeners is `-1` when using this API. Corresponds to
+ [`liveblocks.broadcastEvent`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ broadcast-event).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (Any):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.room import broadcast_event
+
+ return broadcast_event._sync(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ def get_storage_document(
+ self,
+ room_id: str,
+ *,
+ format_: GetStorageDocumentFormat | Unset = UNSET,
+ ) -> GetStorageDocumentResponse:
+ r"""Get Storage document
+
+ Returns the contents of the room’s Storage tree. Corresponds to
+ [`liveblocks.getStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ rooms-roomId-storage).
+
+ The default outputted format is called “plain LSON”, which includes information on the Live data
+ structures in the tree. These nodes show up in the output as objects with two properties, for
+ example:
+
+ ```json
+ {
+ \"liveblocksType\": \"LiveObject\",
+ \"data\": ...
+ }
+ ```
+
+ If you’re not interested in this information, you can use the simpler `?format=json` query param,
+ see below.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ format_ (GetStorageDocumentFormat | Unset): Use the `json` format to output a simplified
+ JSON representation of the Storage tree. In that format, each LiveObject and LiveMap will
+ be formatted as a simple JSON object, and each LiveList will be formatted as a simple JSON
+ array. This is a lossy format because information about the original data structures is
+ not retained, but it may be easier to work with. Example: json.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetStorageDocumentResponse
+ """
+
+ from .api.storage import get_storage_document
+
+ return get_storage_document._sync(
+ room_id=room_id,
+ format_=format_,
+ client=self._client,
+ )
+
+ def initialize_storage_document(
+ self,
+ room_id: str,
+ *,
+ body: InitializeStorageDocumentBody | Unset = UNSET,
+ ) -> InitializeStorageDocumentResponse:
+ r"""Initialize Storage document
+
+ This endpoint initializes or reinitializes a room’s Storage. The room must already exist. Calling
+ this endpoint will disconnect all users from the room if there are any, triggering a reconnect.
+ Corresponds to [`liveblocks.initializeStorageDocument`](https://liveblocks.io/docs/api-
+ reference/liveblocks-node#post-rooms-roomId-storage).
+
+ The format of the request body is the same as what’s returned by the get Storage endpoint.
+
+ For each Liveblocks data structure that you want to create, you need a JSON element having two
+ properties:
+ - `\"liveblocksType\"` => `\"LiveObject\" | \"LiveList\" | \"LiveMap\"`
+ - `\"data\"` => contains the nested data structures (children) and data.
+
+ The root’s type can only be LiveObject.
+
+ A utility function, `toPlainLson` is included in `@liveblocks/client` from `1.0.9` to help convert
+ `LiveObject`, `LiveList`, and `LiveMap` to the structure expected by the endpoint.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (InitializeStorageDocumentBody | Unset):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ InitializeStorageDocumentResponse
+ """
+
+ from .api.storage import initialize_storage_document
+
+ return initialize_storage_document._sync(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ def delete_storage_document(
+ self,
+ room_id: str,
+ ) -> None:
+ """Delete Storage document
+
+ This endpoint deletes all of the room’s Storage data. Calling this endpoint will disconnect all
+ users from the room if there are any. Corresponds to
+ [`liveblocks.deleteStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-rooms-roomId-storage).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.storage import delete_storage_document
+
+ return delete_storage_document._sync(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ def patch_storage_document(
+ self,
+ room_id: str,
+ *,
+ body: list[
+ AddJsonPatchOperation
+ | CopyJsonPatchOperation
+ | MoveJsonPatchOperation
+ | RemoveJsonPatchOperation
+ | ReplaceJsonPatchOperation
+ | TestJsonPatchOperation
+ ],
+ ) -> None:
+ """Apply JSON Patch to Storage
+
+ Applies a sequence of [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations to the
+ room's Storage document, useful for modifying Storage. Operations are applied in order; if any
+ operation fails, the document is not changed and a 422 response with a helpful message is returned.
+
+ **Paths and data types:** Be as specific as possible with your target path. Every parent in the
+ chain of path segments must be a LiveObject, LiveList, or LiveMap. Complex nested objects passed in
+ `add` or `replace` operations are automatically converted to LiveObjects and LiveLists.
+
+ **Performance:** For large Storage documents, applying a patch can be expensive because the full
+ state is reconstructed on the server to apply the operations. Very large documents may not be
+ suitable for this endpoint.
+
+ For a **full guide with examples**, see [Modifying storage via REST API with JSON
+ Patch](https://liveblocks.io/docs/guides/modifying-storage-via-rest-api-with-json-patch).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (list[AddJsonPatchOperation | CopyJsonPatchOperation | MoveJsonPatchOperation |
+ RemoveJsonPatchOperation | ReplaceJsonPatchOperation | TestJsonPatchOperation]): Example:
+ [{'op': 'add', 'path': '/score', 'value': 42}, {'op': 'remove', 'path': '/oldKey'}].
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.storage import patch_storage_document
+
+ return patch_storage_document._sync(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ def get_yjs_document(
+ self,
+ room_id: str,
+ *,
+ formatting: bool | Unset = UNSET,
+ key: str | Unset = UNSET,
+ type_: GetYjsDocumentType | Unset = UNSET,
+ ) -> GetYjsDocumentResponse:
+ """Get Yjs document
+
+ This endpoint returns a JSON representation of the room’s Yjs document. Corresponds to
+ [`liveblocks.getYjsDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-
+ roomId-ydoc).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ formatting (bool | Unset): If present, YText will return formatting.
+ key (str | Unset): Returns only a single key’s value, e.g. `doc.get(key).toJSON()`.
+ Example: root.
+ type_ (GetYjsDocumentType | Unset): Used with key to override the inferred type, i.e.
+ `"ymap"` will return `doc.get(key, Y.Map)`. Example: ymap.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetYjsDocumentResponse
+ """
+
+ from .api.yjs import get_yjs_document
+
+ return get_yjs_document._sync(
+ room_id=room_id,
+ formatting=formatting,
+ key=key,
+ type_=type_,
+ client=self._client,
+ )
+
+ def send_yjs_binary_update(
+ self,
+ room_id: str,
+ *,
+ body: File,
+ guid: str | Unset = UNSET,
+ ) -> None:
+ """Send a binary Yjs update
+
+ This endpoint is used to send a Yjs binary update to the room’s Yjs document. You can use this
+ endpoint to initialize Yjs data for the room or to update the room’s Yjs document. To send an update
+ to a subdocument instead of the main document, pass its `guid`. Corresponds to
+ [`liveblocks.sendYjsBinaryUpdate`](https://liveblocks.io/docs/api-reference/liveblocks-node#put-
+ rooms-roomId-ydoc).
+
+ The update is typically obtained by calling `Y.encodeStateAsUpdate(doc)`. See the [Yjs
+ documentation](https://docs.yjs.dev/api/document-updates) for more details. When manually making
+ this HTTP call, set the HTTP header `Content-Type` to `application/octet-stream`, and send the
+ binary update (a `Uint8Array`) in the body of the HTTP request. This endpoint does not accept JSON,
+ unlike most other endpoints.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ guid (str | Unset): ID of the subdocument Example: subdoc-guid-123.
+ body (File):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.yjs import send_yjs_binary_update
+
+ return send_yjs_binary_update._sync(
+ room_id=room_id,
+ body=body,
+ guid=guid,
+ client=self._client,
+ )
+
+ def get_yjs_document_as_binary_update(
+ self,
+ room_id: str,
+ *,
+ guid: str | Unset = UNSET,
+ ) -> File:
+ """Get Yjs document encoded as a binary Yjs update
+
+ This endpoint returns the room's Yjs document encoded as a single binary update. This can be used by
+ `Y.applyUpdate(responseBody)` to get a copy of the document in your back end. See [Yjs
+ documentation](https://docs.yjs.dev/api/document-updates) for more information on working with
+ updates. To return a subdocument instead of the main document, pass its `guid`. Corresponds to
+ [`liveblocks.getYjsDocumentAsBinaryUpdate`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-rooms-roomId-ydoc-binary).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ guid (str | Unset): ID of the subdocument Example: subdoc-guid-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ File
+ """
+
+ from .api.yjs import get_yjs_document_as_binary_update
+
+ return get_yjs_document_as_binary_update._sync(
+ room_id=room_id,
+ guid=guid,
+ client=self._client,
+ )
+
+ def get_yjs_versions(
+ self,
+ room_id: str,
+ *,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+ ) -> GetYjsVersionsResponse:
+ """Get Yjs version history
+
+ This endpoint returns a list of version history snapshots for the room's Yjs document. The versions
+ are returned sorted by creation date, from newest to oldest.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ limit (int | Unset): A limit on the number of versions to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ cursor (str | Unset): A cursor used for pagination. Get the value from the `nextCursor`
+ response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetYjsVersionsResponse
+ """
+
+ from .api.yjs import get_yjs_versions
+
+ return get_yjs_versions._sync(
+ room_id=room_id,
+ limit=limit,
+ cursor=cursor,
+ client=self._client,
+ )
+
+ def get_yjs_version(
+ self,
+ room_id: str,
+ version_id: str,
+ ) -> File:
+ """Get Yjs document version
+
+ This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ version_id (str): ID of the version Example: vh_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ File
+ """
+
+ from .api.yjs import get_yjs_version
+
+ return get_yjs_version._sync(
+ room_id=room_id,
+ version_id=version_id,
+ client=self._client,
+ )
+
+ def create_yjs_version(
+ self,
+ room_id: str,
+ ) -> CreateYjsVersionResponse:
+ """Create Yjs version snapshot
+
+ This endpoint creates a new version history snapshot for the room's Yjs document.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ CreateYjsVersionResponse
+ """
+
+ from .api.yjs import create_yjs_version
+
+ return create_yjs_version._sync(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ def get_threads(
+ self,
+ room_id: str,
+ *,
+ query: str | Unset = UNSET,
+ ) -> GetThreadsResponse:
+ """Get room threads
+
+ This endpoint returns the threads in the requested room. Corresponds to
+ [`liveblocks.getThreads`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-
+ threads).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ query (str | Unset): Query to filter threads. You can filter by `metadata` and `resolved`,
+ for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND resolved:true`.
+ Learn more about [filtering threads with query
+ language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language).
+ Example: metadata["color"]:"blue".
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetThreadsResponse
+ """
+
+ from .api.comments import get_threads
+
+ return get_threads._sync(
+ room_id=room_id,
+ query=query,
+ client=self._client,
+ )
+
+ def create_thread(
+ self,
+ room_id: str,
+ *,
+ body: CreateThreadRequestBody,
+ ) -> Thread:
+ r"""Create thread
+
+ This endpoint creates a new thread and the first comment in the thread. Corresponds to
+ [`liveblocks.createThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-threads).
+
+ A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to
+ construct a comment’s body, which can be submitted under `comment.body`.
+
+ ```json
+ {
+ \"version\": 1,
+ \"content\": [
+ {
+ \"type\": \"paragraph\",
+ \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]
+ }
+ ]
+ }
+ ```
+
+ `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value
+ length has a limit of 4000 characters maximum for strings.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (CreateThreadRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Thread
+ """
+
+ from .api.comments import create_thread
+
+ return create_thread._sync(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ def get_thread(
+ self,
+ room_id: str,
+ thread_id: str,
+ ) -> Thread:
+ """Get thread
+
+ This endpoint returns a thread by its ID. Corresponds to
+ [`liveblocks.getThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-
+ threads-threadId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Thread
+ """
+
+ from .api.comments import get_thread
+
+ return get_thread._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ client=self._client,
+ )
+
+ def delete_thread(
+ self,
+ room_id: str,
+ thread_id: str,
+ ) -> None:
+ """Delete thread
+
+ This endpoint deletes a thread by its ID. Corresponds to
+ [`liveblocks.deleteThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-
+ roomId-threads-threadId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.comments import delete_thread
+
+ return delete_thread._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ client=self._client,
+ )
+
+ def edit_thread_metadata(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: EditThreadMetadataRequestBody,
+ ) -> ThreadMetadata:
+ """Edit thread metadata
+
+ This endpoint edits the metadata of a thread. The metadata is a JSON object that can be used to
+ store any information you want about the thread, in `string`, `number`, or `boolean` form. Set a
+ property to `null` to remove it. Corresponds to
+ [`liveblocks.editThreadMetadata`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-metadata).
+
+ `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value
+ length has a limit of 4000 characters maximum for strings.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (EditThreadMetadataRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ThreadMetadata
+ """
+
+ from .api.comments import edit_thread_metadata
+
+ return edit_thread_metadata._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ def mark_thread_as_resolved(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: MarkThreadAsResolvedRequestBody,
+ ) -> Thread:
+ """Mark thread as resolved
+
+ This endpoint marks a thread as resolved. The request body must include a `userId` to identify who
+ resolved the thread. Returns the updated thread. Corresponds to
+ [`liveblocks.markThreadAsResolved`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-mark-as-resolved).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (MarkThreadAsResolvedRequestBody): Example: {'userId': 'alice'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Thread
+ """
+
+ from .api.comments import mark_thread_as_resolved
+
+ return mark_thread_as_resolved._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ def mark_thread_as_unresolved(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: MarkThreadAsUnresolvedRequestBody,
+ ) -> Thread:
+ """Mark thread as unresolved
+
+ This endpoint marks a thread as unresolved. The request body must include a `userId` to identify who
+ unresolved the thread. Returns the updated thread. Corresponds to
+ [`liveblocks.markThreadAsUnresolved`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-mark-as-unresolved).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (MarkThreadAsUnresolvedRequestBody): Example: {'userId': 'alice'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Thread
+ """
+
+ from .api.comments import mark_thread_as_unresolved
+
+ return mark_thread_as_unresolved._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ def subscribe_to_thread(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: SubscribeToThreadRequestBody,
+ ) -> Subscription:
+ """Subscribe to thread
+
+ This endpoint subscribes to a thread. Corresponds to
+ [`liveblocks.subscribeToThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-subscribe).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (SubscribeToThreadRequestBody): Example: {'userId': 'alice'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Subscription
+ """
+
+ from .api.comments import subscribe_to_thread
+
+ return subscribe_to_thread._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ def unsubscribe_from_thread(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: UnsubscribeFromThreadRequestBody,
+ ) -> None:
+ """Unsubscribe from thread
+
+ This endpoint unsubscribes from a thread. Corresponds to
+ [`liveblocks.unsubscribeFromThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-unsubscribe).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (UnsubscribeFromThreadRequestBody): Example: {'userId': 'alice'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.comments import unsubscribe_from_thread
+
+ return unsubscribe_from_thread._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ def get_thread_subscriptions(
+ self,
+ room_id: str,
+ thread_id: str,
+ ) -> GetThreadSubscriptionsResponse:
+ """Get thread subscriptions
+
+ This endpoint gets the list of subscriptions to a thread. Corresponds to
+ [`liveblocks.getThreadSubscriptions`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ rooms-roomId-threads-threadId-subscriptions).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetThreadSubscriptionsResponse
+ """
+
+ from .api.comments import get_thread_subscriptions
+
+ return get_thread_subscriptions._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ client=self._client,
+ )
+
+ def create_comment(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: CreateCommentRequestBody,
+ ) -> Comment:
+ r"""Create comment
+
+ This endpoint creates a new comment, adding it as a reply to a thread. Corresponds to
+ [`liveblocks.createComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-threads-threadId-comments).
+
+ A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to
+ construct a comment’s body, which can be submitted under `body`.
+
+ ```json
+ {
+ \"version\": 1,
+ \"content\": [
+ {
+ \"type\": \"paragraph\",
+ \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]
+ }
+ ]
+ }
+ ```
+
+ `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value
+ length has a limit of 4000 characters maximum for strings.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (CreateCommentRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Comment
+ """
+
+ from .api.comments import create_comment
+
+ return create_comment._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ def get_comment(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ ) -> Comment:
+ """Get comment
+
+ This endpoint returns a comment by its ID. Corresponds to
+ [`liveblocks.getComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-
+ threads-threadId-comments-commentId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Comment
+ """
+
+ from .api.comments import get_comment
+
+ return get_comment._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ client=self._client,
+ )
+
+ def edit_comment(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: EditCommentRequestBody,
+ ) -> Comment:
+ r"""Edit comment
+
+ This endpoint edits the specified comment. Corresponds to
+ [`liveblocks.editComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-threads-threadId-comments-commentId).
+
+ A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to
+ construct a comment’s body, which can be submitted under `body`.
+
+ ```json
+ {
+ \"version\": 1,
+ \"content\": [
+ {
+ \"type\": \"paragraph\",
+ \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]
+ }
+ ]
+ }
+ ```
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+ body (EditCommentRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Comment
+ """
+
+ from .api.comments import edit_comment
+
+ return edit_comment._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ client=self._client,
+ )
+
+ def delete_comment(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ ) -> None:
+ """Delete comment
+
+ This endpoint deletes a comment. A deleted comment is no longer accessible from the API or the
+ dashboard and it cannot be restored. Corresponds to
+ [`liveblocks.deleteComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-threads-threadId-comments-commentId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.comments import delete_comment
+
+ return delete_comment._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ client=self._client,
+ )
+
+ def add_comment_reaction(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: AddCommentReactionRequestBody,
+ ) -> CommentReaction:
+ """Add comment reaction
+
+ This endpoint adds a reaction to a comment. Corresponds to
+ [`liveblocks.addCommentReaction`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-comments-commentId-add-reaction).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+ body (AddCommentReactionRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ CommentReaction
+ """
+
+ from .api.comments import add_comment_reaction
+
+ return add_comment_reaction._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ client=self._client,
+ )
+
+ def remove_comment_reaction(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: RemoveCommentReactionRequestBody | Unset = UNSET,
+ ) -> None:
+ """Remove comment reaction
+
+ This endpoint removes a comment reaction. A deleted comment reaction is no longer accessible from
+ the API or the dashboard and it cannot be restored. Corresponds to
+ [`liveblocks.removeCommentReaction`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-comments-commentId-add-reaction).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+ body (RemoveCommentReactionRequestBody | Unset):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.comments import remove_comment_reaction
+
+ return remove_comment_reaction._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ client=self._client,
+ )
+
+ def edit_comment_metadata(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: EditCommentMetadataRequestBody,
+ ) -> CommentMetadata:
+ """Edit comment metadata
+
+ This endpoint edits the metadata of a comment. The metadata is a JSON object that can be used to
+ store any information you want about the comment, in `string`, `number`, or `boolean` form. Set a
+ property to `null` to remove it. Corresponds to
+ [`liveblocks.editCommentMetadata`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-comments-commentId-metadata).
+
+ `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value
+ length has a limit of 4000 characters maximum for strings.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+ body (EditCommentMetadataRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ CommentMetadata
+ """
+
+ from .api.comments import edit_comment_metadata
+
+ return edit_comment_metadata._sync(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ client=self._client,
+ )
+
+ def authorize_user(
+ self,
+ *,
+ body: AuthorizeUserRequestBody,
+ ) -> AuthorizeUserResponse:
+ r"""Get access token with secret key
+
+ This endpoint lets your application server (your back end) obtain a token that one of its clients
+ (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own
+ application’s custom authentication endpoint. When making this request, you’ll have to use your
+ secret key.
+
+ **Important:** The difference with an [ID token](#post-identify-user) is that an access token holds
+ all the permissions, and is the source of truth. With ID tokens, permissions are set in the
+ Liveblocks back end (through REST API calls) and \"checked at the door\" every time they are used to
+ enter a room.
+
+ **Note:** When using the `@liveblocks/node` package, you can use
+ [`Liveblocks.prepareSession`](https://liveblocks.io/docs/api-reference/liveblocks-node#access-
+ tokens) in your back end to build this request.
+
+ You can pass the property `userId` in the request’s body. This can be whatever internal identifier
+ you use for your user accounts as long as it uniquely identifies an account. The property `userId`
+ is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId`
+ corresponds to one MAU.
+
+ Additionally, you can set custom metadata to the token, which will be publicly accessible by other
+ clients through the `user.info` property. This is useful for storing static data like avatar images
+ or the user’s display name.
+
+ Lastly, you’ll specify the exact permissions to give to the user using the `permissions` field. This
+ is done in an object where the keys are room names, or room name patterns (ending in a `*`), and a
+ list of permissions to assign the user for any room that matches that name exactly (or starts with
+ the pattern’s prefix). For tips, see [Manage permissions with access
+ tokens](https://liveblocks.io/docs/authentication/access-token).
+
+ Args:
+ body (AuthorizeUserRequestBody): Example: {'userId': 'user-123', 'userInfo': {'name':
+ 'bob', 'avatar': 'https://example.org/images/user123.jpg'}, 'organizationId': 'acme-corp',
+ 'permissions': {'my-room-1': ['room:write'], 'my-room-2': ['room:write'], 'my-room-*':
+ ['room:read']}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ AuthorizeUserResponse
+ """
+
+ from .api.auth import authorize_user
+
+ return authorize_user._sync(
+ body=body,
+ client=self._client,
+ )
+
+ def identify_user(
+ self,
+ *,
+ body: IdentifyUserRequestBody,
+ ) -> IdentifyUserResponse:
+ r"""Get ID token with secret key
+
+ This endpoint lets your application server (your back end) obtain a token that one of its clients
+ (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own
+ application’s custom authentication endpoint. When using this endpoint to obtain ID tokens, you
+ should manage your permissions by assigning user and/or group permissions to rooms explicitly, see
+ our [Manage permissions with ID tokens](https://liveblocks.io/docs/authentication/id-token) section.
+
+ **Important:** The difference with an [access token](#post-authorize-user) is that an ID token
+ doesn’t hold any permissions itself. With ID tokens, permissions are set in the Liveblocks back end
+ (through REST API calls) and \"checked at the door\" every time they are used to enter a room. With
+ access tokens, all permissions are set in the token itself, and thus controlled from your back end
+ entirely.
+
+ **Note:** When using the `@liveblocks/node` package, you can use
+ [`Liveblocks.identifyUser`](https://liveblocks.io/docs/api-reference/liveblocks-node) in your back
+ end to build this request.
+
+ You can pass the property `userId` in the request’s body. This can be whatever internal identifier
+ you use for your user accounts as long as it uniquely identifies an account. The property `userId`
+ is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId`
+ corresponds to one MAU.
+
+ If you want to use group permissions, you can also declare which `groupIds` this user belongs to.
+ The group ID values are yours, but they will have to match the group IDs you assign permissions to
+ when assigning permissions to rooms, see [Manage permissions with ID
+ tokens](https://liveblocks.io/docs/authentication/id-token)).
+
+ Additionally, you can set custom metadata to the token, which will be publicly accessible by other
+ clients through the `user.info` property. This is useful for storing static data like avatar images
+ or the user’s display name.
+
+ Args:
+ body (IdentifyUserRequestBody): Example: {'userId': 'user-123', 'organizationId': 'acme-
+ corp', 'groupIds': ['marketing', 'engineering'], 'userInfo': {'name': 'bob', 'avatar':
+ 'https://example.org/images/user123.jpg'}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ IdentifyUserResponse
+ """
+
+ from .api.auth import identify_user
+
+ return identify_user._sync(
+ body=body,
+ client=self._client,
+ )
+
+ def get_inbox_notification(
+ self,
+ user_id: str,
+ inbox_notification_id: str,
+ ) -> InboxNotificationCustomData | InboxNotificationThreadData:
+ """Get inbox notification
+
+ This endpoint returns a user’s inbox notification by its ID. Corresponds to
+ [`liveblocks.getInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ users-userId-inboxNotifications-inboxNotificationId).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+ inbox_notification_id (str): ID of the inbox notification Example: in_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ InboxNotificationCustomData | InboxNotificationThreadData
+ """
+
+ from .api.notifications import get_inbox_notification
+
+ return get_inbox_notification._sync(
+ user_id=user_id,
+ inbox_notification_id=inbox_notification_id,
+ client=self._client,
+ )
+
+ def delete_inbox_notification(
+ self,
+ user_id: str,
+ inbox_notification_id: str,
+ ) -> None:
+ """Delete inbox notification
+
+ This endpoint deletes a user’s inbox notification by its ID. Corresponds to
+ [`liveblocks.deleteInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-users-userId-inbox-notifications-inboxNotificationId).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+ inbox_notification_id (str): ID of the inbox notification Example: in_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.notifications import delete_inbox_notification
+
+ return delete_inbox_notification._sync(
+ user_id=user_id,
+ inbox_notification_id=inbox_notification_id,
+ client=self._client,
+ )
+
+ def get_inbox_notifications(
+ self,
+ user_id: str,
+ *,
+ organization_id: str | Unset = UNSET,
+ query: str | Unset = UNSET,
+ limit: int | Unset = 50,
+ starting_after: str | Unset = UNSET,
+ ) -> GetInboxNotificationsResponse:
+ """Get all inbox notifications
+
+ This endpoint returns all the user’s inbox notifications. Corresponds to
+ [`liveblocks.getInboxNotifications`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ users-userId-inboxNotifications).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+ organization_id (str | Unset): The organization ID to filter notifications for. Example:
+ org_123456789.
+ query (str | Unset): Query to filter notifications. You can filter by `unread`, for
+ example, `unread:true`. Example: metadata["color"]:"blue".
+ limit (int | Unset): A limit on the number of inbox notifications to be returned. The
+ limit can range between 1 and 50, and defaults to 50. Default: 50. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetInboxNotificationsResponse
+ """
+
+ from .api.notifications import get_inbox_notifications
+
+ return get_inbox_notifications._sync(
+ user_id=user_id,
+ organization_id=organization_id,
+ query=query,
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ def delete_all_inbox_notifications(
+ self,
+ user_id: str,
+ ) -> None:
+ """Delete all inbox notifications
+
+ This endpoint deletes all the user’s inbox notifications. Corresponds to
+ [`liveblocks.deleteAllInboxNotifications`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-users-userId-inbox-notifications).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.notifications import delete_all_inbox_notifications
+
+ return delete_all_inbox_notifications._sync(
+ user_id=user_id,
+ client=self._client,
+ )
+
+ def get_notification_settings(
+ self,
+ user_id: str,
+ ) -> NotificationSettings:
+ """Get notification settings
+
+ This endpoint returns a user's notification settings for the project. Corresponds to
+ [`liveblocks.getNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ users-userId-notification-settings).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ NotificationSettings
+ """
+
+ from .api.notifications import get_notification_settings
+
+ return get_notification_settings._sync(
+ user_id=user_id,
+ client=self._client,
+ )
+
+ def update_notification_settings(
+ self,
+ user_id: str,
+ *,
+ body: UpdateNotificationSettingsRequestBody,
+ ) -> NotificationSettings:
+ """Update notification settings
+
+ This endpoint updates a user's notification settings for the project. Corresponds to
+ [`liveblocks.updateNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#post-users-userId-notification-settings).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+ body (UpdateNotificationSettingsRequestBody): Partial notification settings - all
+ properties are optional Example: {'email': {'thread': True, 'textMention': False},
+ 'slack': {'textMention': False}, 'webPush': {'thread': True}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ NotificationSettings
+ """
+
+ from .api.notifications import update_notification_settings
+
+ return update_notification_settings._sync(
+ user_id=user_id,
+ body=body,
+ client=self._client,
+ )
+
+ def delete_notification_settings(
+ self,
+ user_id: str,
+ ) -> None:
+ """Delete notification settings
+
+ This endpoint deletes a user's notification settings for the project. Corresponds to
+ [`liveblocks.deleteNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-users-userId-notification-settings).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.notifications import delete_notification_settings
+
+ return delete_notification_settings._sync(
+ user_id=user_id,
+ client=self._client,
+ )
+
+ def get_room_subscription_settings(
+ self,
+ room_id: str,
+ user_id: str,
+ ) -> RoomSubscriptionSettings:
+ """Get room subscription settings
+
+ This endpoint returns a user’s subscription settings for a specific room. Corresponds to
+ [`liveblocks.getRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-rooms-roomId-users-userId-subscription-settings).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ user_id (str): ID of the user Example: user-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ RoomSubscriptionSettings
+ """
+
+ from .api.notifications import get_room_subscription_settings
+
+ return get_room_subscription_settings._sync(
+ room_id=room_id,
+ user_id=user_id,
+ client=self._client,
+ )
+
+ def update_room_subscription_settings(
+ self,
+ room_id: str,
+ user_id: str,
+ *,
+ body: UpdateRoomSubscriptionSettingsRequestBody,
+ ) -> RoomSubscriptionSettings:
+ """Update room subscription settings
+
+ This endpoint updates a user’s subscription settings for a specific room. Corresponds to
+ [`liveblocks.updateRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#post-rooms-roomId-users-userId-subscription-settings).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ user_id (str): ID of the user Example: user-123.
+ body (UpdateRoomSubscriptionSettingsRequestBody): Partial room subscription settings - all
+ properties are optional Example: {'threads': 'replies_and_mentions', 'textMentions':
+ 'none'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ RoomSubscriptionSettings
+ """
+
+ from .api.notifications import update_room_subscription_settings
+
+ return update_room_subscription_settings._sync(
+ room_id=room_id,
+ user_id=user_id,
+ body=body,
+ client=self._client,
+ )
+
+ def delete_room_subscription_settings(
+ self,
+ room_id: str,
+ user_id: str,
+ ) -> None:
+ """Delete room subscription settings
+
+ This endpoint deletes a user’s subscription settings for a specific room. Corresponds to
+ [`liveblocks.deleteRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-rooms-roomId-users-userId-subscription-settings).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ user_id (str): ID of the user Example: user-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.notifications import delete_room_subscription_settings
+
+ return delete_room_subscription_settings._sync(
+ room_id=room_id,
+ user_id=user_id,
+ client=self._client,
+ )
+
+ def get_user_room_subscription_settings(
+ self,
+ user_id: str,
+ *,
+ starting_after: str | Unset = UNSET,
+ limit: int | Unset = 50,
+ organization_id: str | Unset = UNSET,
+ ) -> GetRoomSubscriptionSettingsResponse:
+ """Get user room subscription settings
+
+ This endpoint returns the list of a user's room subscription settings. Corresponds to
+ [`liveblocks.getUserRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-users-userId-room-subscription-settings).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+ limit (int | Unset): A limit on the number of elements to be returned. The limit can range
+ between 1 and 50, and defaults to 50. Default: 50. Example: 20.
+ organization_id (str | Unset): The organization ID to filter room subscription settings
+ for. Example: org_123456789.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetRoomSubscriptionSettingsResponse
+ """
+
+ from .api.notifications import get_user_room_subscription_settings
+
+ return get_user_room_subscription_settings._sync(
+ user_id=user_id,
+ starting_after=starting_after,
+ limit=limit,
+ organization_id=organization_id,
+ client=self._client,
+ )
+
+ def trigger_inbox_notification(
+ self,
+ *,
+ body: TriggerInboxNotificationRequestBody | Unset = UNSET,
+ ) -> None:
+ """Trigger inbox notification
+
+ This endpoint triggers an inbox notification. Corresponds to
+ [`liveblocks.triggerInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#post-inbox-notifications-trigger).
+
+ Args:
+ body (TriggerInboxNotificationRequestBody | Unset): Example: {'userId': 'alice', 'kind':
+ 'file-uploaded', 'subjectId': 'file123', 'activityData': {'url': 'url-to-file'}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.notifications import trigger_inbox_notification
+
+ return trigger_inbox_notification._sync(
+ body=body,
+ client=self._client,
+ )
+
+ def get_groups(
+ self,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ ) -> GetGroupsResponse:
+ """Get groups
+
+ This endpoint returns a list of all groups in your project. Corresponds to
+ [`liveblocks.getGroups`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-groups).
+
+ Args:
+ limit (int | Unset): A limit on the number of groups to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetGroupsResponse
+ """
+
+ from .api.groups import get_groups
+
+ return get_groups._sync(
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ def create_group(
+ self,
+ *,
+ body: CreateGroupRequestBody | Unset = UNSET,
+ ) -> Group:
+ """Create group
+
+ This endpoint creates a new group. Corresponds to
+ [`liveblocks.createGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-group).
+
+ Args:
+ body (CreateGroupRequestBody | Unset): Example: {'id': 'engineering', 'memberIds':
+ ['alice', 'bob'], 'organizationId': 'org_123456789', 'scopes': {'mention': True}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Group
+ """
+
+ from .api.groups import create_group
+
+ return create_group._sync(
+ body=body,
+ client=self._client,
+ )
+
+ def get_group(
+ self,
+ group_id: str,
+ ) -> Group:
+ """Get group
+
+ This endpoint returns a specific group by ID. Corresponds to
+ [`liveblocks.getGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-group).
+
+ Args:
+ group_id (str): The ID of the group to retrieve. Example: engineering.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Group
+ """
+
+ from .api.groups import get_group
+
+ return get_group._sync(
+ group_id=group_id,
+ client=self._client,
+ )
+
+ def delete_group(
+ self,
+ group_id: str,
+ ) -> None:
+ """Delete group
+
+ This endpoint deletes a group. Corresponds to
+ [`liveblocks.deleteGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-group).
+
+ Args:
+ group_id (str): The ID of the group to delete. Example: engineering.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.groups import delete_group
+
+ return delete_group._sync(
+ group_id=group_id,
+ client=self._client,
+ )
+
+ def add_group_members(
+ self,
+ group_id: str,
+ *,
+ body: AddGroupMembersRequestBody,
+ ) -> Group:
+ """Add group members
+
+ This endpoint adds new members to an existing group. Corresponds to
+ [`liveblocks.addGroupMembers`](https://liveblocks.io/docs/api-reference/liveblocks-node#add-group-
+ members).
+
+ Args:
+ group_id (str): The ID of the group to add members to. Example: engineering.
+ body (AddGroupMembersRequestBody): Example: {'memberIds': ['charlie', 'dave']}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Group
+ """
+
+ from .api.groups import add_group_members
+
+ return add_group_members._sync(
+ group_id=group_id,
+ body=body,
+ client=self._client,
+ )
+
+ def remove_group_members(
+ self,
+ group_id: str,
+ *,
+ body: RemoveGroupMembersRequestBody,
+ ) -> Group:
+ """Remove group members
+
+ This endpoint removes members from an existing group. Corresponds to
+ [`liveblocks.removeGroupMembers`](https://liveblocks.io/docs/api-reference/liveblocks-node#remove-
+ group-members).
+
+ Args:
+ group_id (str): The ID of the group to remove members from. Example: engineering.
+ body (RemoveGroupMembersRequestBody): Example: {'memberIds': ['charlie']}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Group
+ """
+
+ from .api.groups import remove_group_members
+
+ return remove_group_members._sync(
+ group_id=group_id,
+ body=body,
+ client=self._client,
+ )
+
+ def get_user_groups(
+ self,
+ user_id: str,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ ) -> GetUserGroupsResponse:
+ """Get user groups
+
+ This endpoint returns all groups that a specific user is a member of. Corresponds to
+ [`liveblocks.getUserGroups`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-user-
+ groups).
+
+ Args:
+ user_id (str): The ID of the user to get groups for. Example: user-123.
+ limit (int | Unset): A limit on the number of groups to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetUserGroupsResponse
+ """
+
+ from .api.groups import get_user_groups
+
+ return get_user_groups._sync(
+ user_id=user_id,
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ def get_ai_copilots(
+ self,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ ) -> GetAiCopilotsResponse:
+ """Get AI copilots
+
+ This endpoint returns a paginated list of AI copilots. The copilots are returned sorted by creation
+ date, from newest to oldest. Corresponds to
+ [`liveblocks.getAiCopilots`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-ai-
+ copilots).
+
+ Args:
+ limit (int | Unset): A limit on the number of copilots to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetAiCopilotsResponse
+ """
+
+ from .api.ai import get_ai_copilots
+
+ return get_ai_copilots._sync(
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ def create_ai_copilot(
+ self,
+ *,
+ body: CreateAiCopilotOptionsAnthropic
+ | CreateAiCopilotOptionsGoogle
+ | CreateAiCopilotOptionsOpenAi
+ | CreateAiCopilotOptionsOpenAiCompatible,
+ ) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ """Create AI copilot
+
+ This endpoint creates a new AI copilot with the given configuration. Corresponds to
+ [`liveblocks.createAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-ai-
+ copilot).
+
+ Args:
+ body (CreateAiCopilotOptionsAnthropic | CreateAiCopilotOptionsGoogle |
+ CreateAiCopilotOptionsOpenAi | CreateAiCopilotOptionsOpenAiCompatible): Example: {'name':
+ 'My Copilot', 'systemPrompt': 'You are a helpful assistant.', 'providerApiKey': 'sk-...',
+ 'provider': 'openai', 'providerModel': 'gpt-4o'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible
+ """
+
+ from .api.ai import create_ai_copilot
+
+ return create_ai_copilot._sync(
+ body=body,
+ client=self._client,
+ )
+
+ def get_ai_copilot(
+ self,
+ copilot_id: str,
+ ) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ """Get AI copilot
+
+ This endpoint returns an AI copilot by its ID. Corresponds to
+ [`liveblocks.getAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-ai-
+ copilot).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible
+ """
+
+ from .api.ai import get_ai_copilot
+
+ return get_ai_copilot._sync(
+ copilot_id=copilot_id,
+ client=self._client,
+ )
+
+ def update_ai_copilot(
+ self,
+ copilot_id: str,
+ *,
+ body: UpdateAiCopilotRequestBody,
+ ) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ r"""Update AI copilot
+
+ This endpoint updates an existing AI copilot's configuration. Corresponds to
+ [`liveblocks.updateAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#update-ai-
+ copilot).
+
+ This endpoint returns a 422 response if the update doesn't apply due to validation failures. For
+ example, if the existing copilot uses the \"openai\" provider and you attempt to update the provider
+ model to an incompatible value for the provider, like \"gemini-2.5-pro\", you'll receive a 422
+ response with an error message explaining where the validation failed.
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ body (UpdateAiCopilotRequestBody): Example: {'name': 'Updated Copilot', 'systemPrompt':
+ 'You are an updated helpful assistant.', 'providerModel': 'gpt-4o', 'settings':
+ {'maxTokens': 8192}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible
+ """
+
+ from .api.ai import update_ai_copilot
+
+ return update_ai_copilot._sync(
+ copilot_id=copilot_id,
+ body=body,
+ client=self._client,
+ )
+
+ def delete_ai_copilot(
+ self,
+ copilot_id: str,
+ ) -> None:
+ """Delete AI copilot
+
+ This endpoint deletes an AI copilot by its ID. A deleted copilot is no longer accessible and cannot
+ be restored. Corresponds to [`liveblocks.deleteAiCopilot`](https://liveblocks.io/docs/api-
+ reference/liveblocks-node#delete-ai-copilot).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.ai import delete_ai_copilot
+
+ return delete_ai_copilot._sync(
+ copilot_id=copilot_id,
+ client=self._client,
+ )
+
+ def get_knowledge_sources(
+ self,
+ copilot_id: str,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ ) -> GetKnowledgeSourcesResponse:
+ """Get knowledge sources
+
+ This endpoint returns a paginated list of knowledge sources for a specific AI copilot. Corresponds
+ to [`liveblocks.getKnowledgeSources`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ knowledge-sources).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ limit (int | Unset): A limit on the number of knowledge sources to be returned. The limit
+ can range between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetKnowledgeSourcesResponse
+ """
+
+ from .api.ai import get_knowledge_sources
+
+ return get_knowledge_sources._sync(
+ copilot_id=copilot_id,
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ def get_knowledge_source(
+ self,
+ copilot_id: str,
+ knowledge_source_id: str,
+ ) -> KnowledgeSourceFileSource | KnowledgeSourceWebSource:
+ """Get knowledge source
+
+ This endpoint returns a specific knowledge source by its ID. Corresponds to
+ [`liveblocks.getKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ knowledge-source).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ knowledge_source_id (str): ID of the knowledge source Example: ks_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ KnowledgeSourceFileSource | KnowledgeSourceWebSource
+ """
+
+ from .api.ai import get_knowledge_source
+
+ return get_knowledge_source._sync(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ client=self._client,
+ )
+
+ def create_web_knowledge_source(
+ self,
+ copilot_id: str,
+ *,
+ body: CreateWebKnowledgeSourceRequestBody,
+ ) -> CreateWebKnowledgeSourceResponse:
+ """Create web knowledge source
+
+ This endpoint creates a web knowledge source for an AI copilot. This allows the copilot to access
+ and learn from web content. Corresponds to
+ [`liveblocks.createWebKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#create-web-knowledge-source).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ body (CreateWebKnowledgeSourceRequestBody): Example: {'copilotId': 'cp_abc123', 'url':
+ 'https://docs.example.com', 'type': 'crawl'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ CreateWebKnowledgeSourceResponse
+ """
+
+ from .api.ai import create_web_knowledge_source
+
+ return create_web_knowledge_source._sync(
+ copilot_id=copilot_id,
+ body=body,
+ client=self._client,
+ )
+
+ def create_file_knowledge_source(
+ self,
+ copilot_id: str,
+ name: str,
+ *,
+ body: File,
+ ) -> CreateFileKnowledgeSourceResponse200:
+ """Create file knowledge source
+
+ This endpoint creates a file knowledge source for an AI copilot by uploading a file. The copilot can
+ then reference the content of the file when responding. Corresponds to
+ [`liveblocks.createFileKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#create-file-knowledge-source).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ name (str): Name of the file Example: document.pdf.
+ body (File):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ CreateFileKnowledgeSourceResponse200
+ """
+
+ from .api.ai import create_file_knowledge_source
+
+ return create_file_knowledge_source._sync(
+ copilot_id=copilot_id,
+ name=name,
+ body=body,
+ client=self._client,
+ )
+
+ def get_file_knowledge_source_markdown(
+ self,
+ copilot_id: str,
+ knowledge_source_id: str,
+ ) -> GetFileKnowledgeSourceMarkdownResponse:
+ """Get file knowledge source content
+
+ This endpoint returns the content of a file knowledge source as markdown. This allows you to see
+ what content the AI copilot has access to from uploaded files. Corresponds to
+ [`liveblocks.getFileKnowledgeSourceMarkdown`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-file-knowledge-source-markdown).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ knowledge_source_id (str): ID of the knowledge source Example: ks_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetFileKnowledgeSourceMarkdownResponse
+ """
+
+ from .api.ai import get_file_knowledge_source_markdown
+
+ return get_file_knowledge_source_markdown._sync(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ client=self._client,
+ )
+
+ def delete_file_knowledge_source(
+ self,
+ copilot_id: str,
+ knowledge_source_id: str,
+ ) -> None:
+ """Delete file knowledge source
+
+ This endpoint deletes a file knowledge source from an AI copilot. The copilot will no longer have
+ access to the content from this file. Corresponds to
+ [`liveblocks.deleteFileKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-file-knowledge-source).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ knowledge_source_id (str): ID of the knowledge source Example: ks_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.ai import delete_file_knowledge_source
+
+ return delete_file_knowledge_source._sync(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ client=self._client,
+ )
+
+ def delete_web_knowledge_source(
+ self,
+ copilot_id: str,
+ knowledge_source_id: str,
+ ) -> None:
+ """Delete web knowledge source
+
+ This endpoint deletes a web knowledge source from an AI copilot. The copilot will no longer have
+ access to the content from this source. Corresponds to
+ [`liveblocks.deleteWebKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-web-knowledge-source).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ knowledge_source_id (str): ID of the knowledge source Example: ks_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.ai import delete_web_knowledge_source
+
+ return delete_web_knowledge_source._sync(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ client=self._client,
+ )
+
+ def get_web_knowledge_source_links(
+ self,
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ ) -> GetWebKnowledgeSourceLinksResponse:
+ """Get web knowledge source links
+
+ This endpoint returns a paginated list of links that were indexed from a web knowledge source. This
+ is useful for understanding what content the AI copilot has access to from web sources. Corresponds
+ to [`liveblocks.getWebKnowledgeSourceLinks`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-web-knowledge-source-links).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ knowledge_source_id (str): ID of the knowledge source Example: ks_abc123.
+ limit (int | Unset): A limit on the number of links to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetWebKnowledgeSourceLinksResponse
+ """
+
+ from .api.ai import get_web_knowledge_source_links
+
+ return get_web_knowledge_source_links._sync(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ def get_management_projects(
+ self,
+ *,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+ ) -> GetManagementProjectsResponse:
+ """List projects
+
+ Returns a paginated list of projects. You can limit the number of projects returned per page and use
+ the provided `nextCursor` for pagination. This endpoint requires the `read:all` scope.
+
+ Args:
+ limit (int | Unset): A limit on the number of projects to return. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ cursor (str | Unset): A cursor used for pagination. Get the value from the `nextCursor`
+ response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetManagementProjectsResponse
+ """
+
+ from .api.management import get_management_projects
+
+ return get_management_projects._sync(
+ limit=limit,
+ cursor=cursor,
+ client=self._client,
+ )
+
+ def create_management_project(
+ self,
+ *,
+ body: CreateManagementProjectRequestBody,
+ ) -> ManagementProject:
+ """Create project
+
+ Creates a new project within your account. This endpoint requires the `write:all` scope. You can
+ specify the project type, name, and version creation timeout. Upon success, returns information
+ about the newly created project, including its ID, keys, region, and settings.
+
+ Args:
+ body (CreateManagementProjectRequestBody): Example: {'name': 'My Project', 'type': 'dev',
+ 'versionCreationTimeout': False}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementProject
+ """
+
+ from .api.management import create_management_project
+
+ return create_management_project._sync(
+ body=body,
+ client=self._client,
+ )
+
+ def get_management_project(
+ self,
+ project_id: str,
+ ) -> ManagementProject:
+ """Get project
+
+ Returns a single project specified by its ID. This endpoint requires the `read:all` scope. If the
+ project cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementProject
+ """
+
+ from .api.management import get_management_project
+
+ return get_management_project._sync(
+ project_id=project_id,
+ client=self._client,
+ )
+
+ def update_management_project(
+ self,
+ project_id: str,
+ *,
+ body: UpdateManagementProjectRequestBody,
+ ) -> ManagementProject:
+ """Update project
+
+ Updates an existing project specified by its ID. This endpoint allows you to modify project details
+ such as the project name and the version creation timeout. The `versionCreationTimeout` can be set
+ to `false` to disable the timeout or to a number of seconds between 30 and 300. Fields omitted from
+ the request body will not be updated. Requires the `write:all` scope.
+
+ If the project cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ body (UpdateManagementProjectRequestBody): Example: {'name': 'Updated Project Name',
+ 'versionCreationTimeout': 60}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementProject
+ """
+
+ from .api.management import update_management_project
+
+ return update_management_project._sync(
+ project_id=project_id,
+ body=body,
+ client=self._client,
+ )
+
+ def delete_management_project(
+ self,
+ project_id: str,
+ ) -> None:
+ """Delete project
+
+ Soft deletes the project specified by its ID. This endpoint requires the `write:all` scope. If the
+ project cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.management import delete_management_project
+
+ return delete_management_project._sync(
+ project_id=project_id,
+ client=self._client,
+ )
+
+ def activate_project_public_api_key(
+ self,
+ project_id: str,
+ ) -> None:
+ """Activate public key
+
+ Activates the public API key associated with the specified project. This endpoint requires the
+ `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.management import activate_project_public_api_key
+
+ return activate_project_public_api_key._sync(
+ project_id=project_id,
+ client=self._client,
+ )
+
+ def deactivate_project_public_api_key(
+ self,
+ project_id: str,
+ ) -> None:
+ """Deactivate public key
+
+ Deactivates the public API key associated with the specified project. This endpoint requires the
+ `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.management import deactivate_project_public_api_key
+
+ return deactivate_project_public_api_key._sync(
+ project_id=project_id,
+ client=self._client,
+ )
+
+ def roll_project_public_api_key(
+ self,
+ project_id: str,
+ *,
+ body: RollProjectPublicApiKeyRequestBody | Unset = UNSET,
+ ) -> RollProjectPublicApiKeyResponse:
+ """Roll public key
+
+ Rolls (rotates) the public API key associated with the specified project, generating a new key value
+ while deprecating the previous one. The new key becomes immediately active. This endpoint requires
+ the `write:all` scope.
+
+ If the public key is not currently enabled for the project, a 403 error response is returned. If the
+ project cannot be found, a 404 error response is returned. An optional `expirationIn` parameter can
+ be provided in the request body to set when the previous key should expire.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ body (RollProjectPublicApiKeyRequestBody | Unset): Example: {'expirationIn': '1 hour'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ RollProjectPublicApiKeyResponse
+ """
+
+ from .api.management import roll_project_public_api_key
+
+ return roll_project_public_api_key._sync(
+ project_id=project_id,
+ body=body,
+ client=self._client,
+ )
+
+ def roll_project_secret_api_key(
+ self,
+ project_id: str,
+ *,
+ body: RollProjectSecretApiKeyRequestBody | Unset = UNSET,
+ ) -> ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse:
+ """Roll secret key
+
+ Rolls (rotates) the secret API key associated with the specified project, generating a new key value
+ while deprecating the previous one. The new key becomes immediately active. This endpoint requires
+ the `write:all` scope.
+
+ If the project cannot be found, a 404 error response is returned. An optional `expirationIn`
+ parameter can be provided in the request body to set when the previous key should expire.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ body (RollProjectSecretApiKeyRequestBody | Unset): Example: {'expirationIn': '3 days'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse
+ """
+
+ from .api.management import roll_project_secret_api_key
+
+ return roll_project_secret_api_key._sync(
+ project_id=project_id,
+ body=body,
+ client=self._client,
+ )
+
+ def get_management_webhooks(
+ self,
+ project_id: str,
+ *,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+ ) -> GetManagementWebhooksResponse:
+ """List webhooks
+
+ Returns a paginated list of webhooks for a project. This endpoint requires the `read:all` scope. The
+ response includes an array of webhook objects associated with the specified project, as well as a
+ `nextCursor` property for pagination. Use the `limit` query parameter to specify the maximum number
+ of webhooks to return (1-100, default 20). If the result is paginated, use the `cursor` parameter
+ from the `nextCursor` value in the previous response to fetch subsequent pages. If the project
+ cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ limit (int | Unset): A limit on the number of webhooks to return. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ cursor (str | Unset): A cursor used for pagination. Get the value from the `nextCursor`
+ response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetManagementWebhooksResponse
+ """
+
+ from .api.management import get_management_webhooks
+
+ return get_management_webhooks._sync(
+ project_id=project_id,
+ limit=limit,
+ cursor=cursor,
+ client=self._client,
+ )
+
+ def create_management_webhook(
+ self,
+ project_id: str,
+ *,
+ body: CreateManagementWebhookRequestBody,
+ ) -> ManagementWebhook:
+ """Create webhook
+
+ Creates a new webhook for a project. This endpoint requires the `write:all` scope. If the project
+ cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ body (CreateManagementWebhookRequestBody): Example: {'url':
+ 'https://example.com/webhooks', 'subscribedEvents': ['storageUpdated', 'userEntered'],
+ 'rateLimit': 100, 'storageUpdatedThrottleSeconds': 10, 'yDocUpdatedThrottleSeconds': 10}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementWebhook
+ """
+
+ from .api.management import create_management_webhook
+
+ return create_management_webhook._sync(
+ project_id=project_id,
+ body=body,
+ client=self._client,
+ )
+
+ def get_management_webhook(
+ self,
+ project_id: str,
+ webhook_id: str,
+ ) -> ManagementWebhook:
+ """Get webhook
+
+ Get one webhook by `webhookId` for a project. Returns webhook settings such as URL, subscribed
+ events, disabled state, throttling, and additional headers. Returns `404` if the project or webhook
+ does not exist. This endpoint requires the `read:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementWebhook
+ """
+
+ from .api.management import get_management_webhook
+
+ return get_management_webhook._sync(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ client=self._client,
+ )
+
+ def update_management_webhook(
+ self,
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: UpdateManagementWebhookRequestBody,
+ ) -> ManagementWebhook:
+ """Update webhook
+
+ Update one webhook by `webhookId` for a project. Send only fields you want to change; omitted fields
+ stay unchanged. Returns `404` if the project or webhook does not exist and `422` for validation
+ errors. This endpoint requires the `write:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+ body (UpdateManagementWebhookRequestBody): Example: {'url':
+ 'https://example.com/webhooks', 'subscribedEvents': ['storageUpdated', 'userEntered'],
+ 'rateLimit': 100, 'disabled': False}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementWebhook
+ """
+
+ from .api.management import update_management_webhook
+
+ return update_management_webhook._sync(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ client=self._client,
+ )
+
+ def delete_management_webhook(
+ self,
+ project_id: str,
+ webhook_id: str,
+ ) -> None:
+ """Delete webhook
+
+ Delete one webhook by `webhookId` for a project. Returns `200` with an empty body on success, or
+ `404` if the project or webhook does not exist. Requires `write:all`.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.management import delete_management_webhook
+
+ return delete_management_webhook._sync(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ client=self._client,
+ )
+
+ def roll_management_webhook_secret(
+ self,
+ project_id: str,
+ webhook_id: str,
+ ) -> RotateManagementWebhookSecretResponse:
+ """Roll webhook secret
+
+ Rotate a webhook signing secret and return the new secret. The previous secret remains valid for 24
+ hours. Returns `404` if the project or webhook does not exist. This endpoint requires the
+ `write:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ RotateManagementWebhookSecretResponse
+ """
+
+ from .api.management import roll_management_webhook_secret
+
+ return roll_management_webhook_secret._sync(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ client=self._client,
+ )
+
+ def get_management_webhook_additional_headers(
+ self,
+ project_id: str,
+ webhook_id: str,
+ ) -> GetManagementWebhookHeadersResponse:
+ """Get webhook headers
+
+ Get a webhook's additional headers. Returns `404` if the project or webhook does not exist. Requires
+ `read:all`.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetManagementWebhookHeadersResponse
+ """
+
+ from .api.management import get_management_webhook_additional_headers
+
+ return get_management_webhook_additional_headers._sync(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ client=self._client,
+ )
+
+ def upsert_management_webhook_additional_headers(
+ self,
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: UpsertManagementWebhookHeadersRequestBody,
+ ) -> UpsertManagementWebhookHeadersResponse:
+ """Patch webhook headers
+
+ Upsert additional headers for a webhook. Provided headers are merged with existing headers, and
+ existing values are overwritten when names match. Returns updated headers, or `404` if the project
+ or webhook does not exist. This endpoint requires the `write:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+ body (UpsertManagementWebhookHeadersRequestBody): Example: {'headers': {'X-Custom-
+ Header': 'value'}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ UpsertManagementWebhookHeadersResponse
+ """
+
+ from .api.management import upsert_management_webhook_additional_headers
+
+ return upsert_management_webhook_additional_headers._sync(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ client=self._client,
+ )
+
+ def delete_management_webhook_additional_headers(
+ self,
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: DeleteManagementWebhookHeadersRequestBody,
+ ) -> DeleteManagementWebhookHeadersResponse:
+ """Delete webhook headers
+
+ Remove selected additional headers from a webhook. Send header names in `headers` field; other
+ headers are unchanged. Returns updated headers, or `404` if the project or webhook does not exist.
+ This endpoint requires the `write:all` scope. At least one header name must be provided; otherwise,
+ a 422 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+ body (DeleteManagementWebhookHeadersRequestBody): Example: {'headers': ['X-Custom-
+ Header', 'X-Another-Header']}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ DeleteManagementWebhookHeadersResponse
+ """
+
+ from .api.management import delete_management_webhook_additional_headers
+
+ return delete_management_webhook_additional_headers._sync(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ client=self._client,
+ )
+
+ def recover_failed_webhook_messages(
+ self,
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: RecoverManagementWebhookFailedMessagesRequestBody,
+ ) -> None:
+ """Recover failed webhook messages
+
+ Requeue failed deliveries for a webhook from the given `since` timestamp. Returns `200` with an
+ empty body when recovery starts, an `404` if the project or webhook does not exist. This endpoint
+ requires the `write:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+ body (RecoverManagementWebhookFailedMessagesRequestBody): Example: {'since':
+ '2026-01-21T00:00:00.000Z'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.management import recover_failed_webhook_messages
+
+ return recover_failed_webhook_messages._sync(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ client=self._client,
+ )
+
+ def send_test_webhook(
+ self,
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: TestManagementWebhookRequestBody,
+ ) -> TestManagementWebhookResponse:
+ """Send test webhook
+
+ Send a test event to a webhook and return the created message metadata. `subscribedEvent` must be
+ one of the webhook's subscribed events, otherwise the endpoint returns `422`. Returns `404` if the
+ project or webhook does not exist. This endpoint requires the `write:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+ body (TestManagementWebhookRequestBody): Example: {'subscribedEvent': 'storageUpdated'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ TestManagementWebhookResponse
+ """
+
+ from .api.management import send_test_webhook
+
+ return send_test_webhook._sync(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ client=self._client,
+ )
+
+
+class AsyncLiveblocks:
+ """Asynchronous client for the Liveblocks API.
+
+ Args:
+ secret: The Liveblocks secret key. Must start with ``sk_``.
+ Get it from https://liveblocks.io/dashboard/apikeys
+ base_url: Point the client to an alternative Liveblocks server.
+ """
+
+ _client: httpx.AsyncClient
+
+ def __init__(self, *, secret: str, base_url: str | None = None) -> None:
+ _assert_secret_key(secret)
+ self._client = httpx.AsyncClient(
+ base_url=base_url or _DEFAULT_BASE_URL,
+ headers={"Authorization": f"Bearer {secret}"},
+ )
+
+ async def close(self) -> None:
+ await self._client.aclose()
+
+ async def __aenter__(self) -> AsyncLiveblocks:
+ await self._client.__aenter__()
+ return self
+
+ async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
+ await self._client.__aexit__(*args, **kwargs)
+
+ def prepare_session(
+ self,
+ user_id: str,
+ user_info: dict[str, Any] | None = None,
+ organization_id: str | None = None,
+ ) -> AsyncSession:
+ from .session import AsyncSession
+
+ return AsyncSession(
+ client=self,
+ user_id=user_id,
+ user_info=user_info,
+ organization_id=organization_id,
+ )
+
+ async def get_rooms(
+ self,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ organization_id: str | Unset = UNSET,
+ query: str | Unset = UNSET,
+ user_id: str | Unset = UNSET,
+ group_ids: str | Unset = UNSET,
+ ) -> GetRoomsResponse:
+ """Get rooms
+
+ This endpoint returns a list of your rooms. The rooms are returned sorted by creation date, from
+ newest to oldest. You can filter rooms by room ID prefixes, metadata, users accesses, and groups
+ accesses. Corresponds to [`liveblocks.getRooms`](https://liveblocks.io/docs/api-
+ reference/liveblocks-node#get-rooms).
+
+ There is a pagination system where the cursor to the next page is returned in the response as
+ `nextCursor`, which can be combined with `startingAfter`.
+ You can also limit the number of rooms by query.
+
+ Filtering by metadata works by giving key values like `metadata.color=red`. Of course you can
+ combine multiple metadata clauses to refine the response like
+ `metadata.color=red&metadata.type=text`. Notice here the operator AND is applied between each
+ clauses.
+
+ Filtering by groups or userId works by giving a list of groups like
+ `groupIds=marketing,GZo7tQ,product` or/and a userId like `userId=user1`.
+ Notice here the operator OR is applied between each `groupIds` and the `userId`.
+
+ Args:
+ limit (int | Unset): A limit on the number of rooms to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+ organization_id (str | Unset): A filter on organization ID. Example: org_123456789.
+ query (str | Unset): Query to filter rooms. You can filter by `roomId` and `metadata`, for
+ example, `metadata["roomType"]:"whiteboard" AND roomId^"liveblocks:engineering"`. Learn
+ more about [filtering rooms with query language](https://liveblocks.io/docs/guides/how-to-
+ filter-rooms-using-query-language). Example: metadata["color"]:"blue".
+ user_id (str | Unset): A filter on users accesses. Example: user-123.
+ group_ids (str | Unset): A filter on groups accesses. Multiple groups can be used.
+ Example: group1,group2.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetRoomsResponse
+ """
+
+ from .api.room import get_rooms
+
+ return await get_rooms._asyncio(
+ limit=limit,
+ starting_after=starting_after,
+ organization_id=organization_id,
+ query=query,
+ user_id=user_id,
+ group_ids=group_ids,
+ client=self._client,
+ )
+
+ async def create_room(
+ self,
+ *,
+ body: CreateRoomRequestBody,
+ idempotent: bool | Unset = UNSET,
+ ) -> Room:
+ r"""Create room
+
+ This endpoint creates a new room. `id` and `defaultAccesses` are required. When provided with a
+ `?idempotent` query argument, will not return a 409 when the room already exists, but instead return
+ the existing room as-is. Corresponds to [`liveblocks.createRoom`](https://liveblocks.io/docs/api-
+ reference/liveblocks-node#post-rooms), or to
+ [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-
+ create-rooms-roomId) when `?idempotent` is provided.
+ - `defaultAccesses` could be `[]` or `[\"room:write\"]` (private or public).
+ - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries.
+ Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum.
+ `metadata` is optional field.
+ - `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain
+ 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+ - `groupsAccesses` are optional fields.
+
+ Args:
+ idempotent (bool | Unset): When provided, will not return a 409 when the room already
+ exists, but instead return the existing room as-is. Corresponds to
+ [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-or-create-rooms-roomId). Example: True.
+ body (CreateRoomRequestBody): Example: {'id': 'my-room-id', 'defaultAccesses':
+ ['room:write'], 'metadata': {'color': 'blue'}, 'usersAccesses': {'alice': ['room:write']},
+ 'groupsAccesses': {'product': ['room:write']}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Room
+ """
+
+ from .api.room import create_room
+
+ return await create_room._asyncio(
+ body=body,
+ idempotent=idempotent,
+ client=self._client,
+ )
+
+ async def get_room(
+ self,
+ room_id: str,
+ ) -> Room:
+ """Get room
+
+ This endpoint returns a room by its ID. Corresponds to
+ [`liveblocks.getRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomid).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Room
+ """
+
+ from .api.room import get_room
+
+ return await get_room._asyncio(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ async def update_room(
+ self,
+ room_id: str,
+ *,
+ body: UpdateRoomRequestBody,
+ ) -> Room:
+ r"""Update room
+
+ This endpoint updates specific properties of a room. Corresponds to
+ [`liveblocks.updateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomid).
+
+ It’s not necessary to provide the entire room’s information.
+ Setting a property to `null` means to delete this property. For example, if you want to remove
+ access to a specific user without losing other users:
+ ``{
+ \"usersAccesses\": {
+ \"john\": null
+ }
+ }``
+ `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.
+
+ - `defaultAccesses` could be `[]` or `[\"room:write\"]` (private or public).
+ - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries.
+ Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum.
+ `metadata` is optional field.
+ - `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain
+ 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+ - `groupsAccesses` could be `[]` or `[\"room:write\"]` for every records. `groupsAccesses` can
+ contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional
+ field.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (UpdateRoomRequestBody): Example: {'defaultAccesses': ['room:write'],
+ 'usersAccesses': {'alice': ['room:write']}, 'groupsAccesses': {'marketing':
+ ['room:write']}, 'metadata': {'color': 'blue'}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Room
+ """
+
+ from .api.room import update_room
+
+ return await update_room._asyncio(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def delete_room(
+ self,
+ room_id: str,
+ ) -> None:
+ """Delete room
+
+ This endpoint deletes a room. A deleted room is no longer accessible from the API or the dashboard
+ and it cannot be restored. Corresponds to [`liveblocks.deleteRoom`](https://liveblocks.io/docs/api-
+ reference/liveblocks-node#delete-rooms-roomid).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.room import delete_room
+
+ return await delete_room._asyncio(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ async def prewarm_room(
+ self,
+ room_id: str,
+ ) -> None:
+ """Prewarm room
+
+ Speeds up connecting to a room for the next 10 seconds. Use this when you know a user will be
+ connecting to a room with [`RoomProvider`](https://liveblocks.io/docs/api-reference/liveblocks-
+ react#RoomProvider) or [`enterRoom`](https://liveblocks.io/docs/api-reference/liveblocks-
+ client#Client.enterRoom) within 10 seconds, and the room will load quicker. Corresponds to
+ [`liveblocks.prewarmRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-
+ roomid-prewarm).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.room import prewarm_room
+
+ return await prewarm_room._asyncio(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ async def upsert_room(
+ self,
+ room_id: str,
+ *,
+ body: UpsertRoomRequestBody,
+ ) -> Room:
+ r"""Upsert (update or create) room
+
+ This endpoint updates specific properties of a room. Corresponds to
+ [`liveblocks.upsertRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#upsert-rooms-
+ roomId).
+
+ It’s not necessary to provide the entire room’s information.
+ Setting a property to `null` means to delete this property. For example, if you want to remove
+ access to a specific user without losing other users:
+ ``{
+ \"usersAccesses\": {
+ \"john\": null
+ }
+ }``
+ `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.
+
+ - `defaultAccesses` could be `[]` or `[\"room:write\"]` (private or public).
+ - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries.
+ Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum.
+ `metadata` is optional field.
+ - `usersAccesses` could be `[]` or `[\"room:write\"]` for every records. `usersAccesses` can contain
+ 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.
+ - `groupsAccesses` could be `[]` or `[\"room:write\"]` for every records. `groupsAccesses` can
+ contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional
+ field.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (UpsertRoomRequestBody): Example: {'update': {'usersAccesses': {'alice':
+ ['room:write']}, 'groupsAccesses': {'marketing': ['room:write']}, 'metadata': {'color':
+ 'blue'}}, 'create': {'defaultAccesses': ['room:write']}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Room
+ """
+
+ from .api.room import upsert_room
+
+ return await upsert_room._asyncio(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def update_room_id(
+ self,
+ room_id: str,
+ *,
+ body: UpdateRoomIdRequestBody | Unset = UNSET,
+ ) -> Room:
+ """Update room ID
+
+ This endpoint permanently updates the room’s ID. All existing references to the old room ID will
+ need to be updated. Returns the updated room. Corresponds to
+ [`liveblocks.updateRoomId`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomid-update-room-id).
+
+ Args:
+ room_id (str): The new ID for the room Example: my-room-id.
+ body (UpdateRoomIdRequestBody | Unset): Example: {'newRoomId': 'new-room-id'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Room
+ """
+
+ from .api.room import update_room_id
+
+ return await update_room_id._asyncio(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_active_users(
+ self,
+ room_id: str,
+ ) -> ActiveUsersResponse:
+ """Get active users
+
+ This endpoint returns a list of users currently present in the requested room. Corresponds to
+ [`liveblocks.getActiveUsers`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-
+ roomid-active-users).
+
+ For optimal performance, we recommend calling this endpoint no more than once every 10 seconds.
+ Duplicates can occur if a user is in the requested room with multiple browser tabs opened.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ActiveUsersResponse
+ """
+
+ from .api.room import get_active_users
+
+ return await get_active_users._asyncio(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ async def set_presence(
+ self,
+ room_id: str,
+ *,
+ body: SetPresenceRequestBody,
+ ) -> None:
+ """Set ephemeral presence
+
+ This endpoint sets ephemeral presence for a user in a room without requiring a WebSocket connection.
+ The presence data will automatically expire after the specified TTL (time-to-live). This is useful
+ for scenarios like showing an AI agent's presence in a room. The presence will be broadcast to all
+ connected users in the room. Corresponds to
+ [`liveblocks.setPresence`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-presence).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (SetPresenceRequestBody): Example: {'userId': 'agent-123', 'data': {'status':
+ 'active', 'cursor': {'x': 100, 'y': 200}}, 'userInfo': {'name': 'AI Assistant', 'avatar':
+ 'https://example.org/images/agent123.jpg'}, 'ttl': 60}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.room import set_presence
+
+ return await set_presence._asyncio(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def broadcast_event(
+ self,
+ room_id: str,
+ *,
+ body: Any,
+ ) -> None:
+ """Broadcast event to a room
+
+ This endpoint enables the broadcast of an event to a room without having to connect to it via the
+ `client` from `@liveblocks/client`. It takes any valid JSON as a request body. The `connectionId`
+ passed to event listeners is `-1` when using this API. Corresponds to
+ [`liveblocks.broadcastEvent`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ broadcast-event).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (Any):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.room import broadcast_event
+
+ return await broadcast_event._asyncio(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_storage_document(
+ self,
+ room_id: str,
+ *,
+ format_: GetStorageDocumentFormat | Unset = UNSET,
+ ) -> GetStorageDocumentResponse:
+ r"""Get Storage document
+
+ Returns the contents of the room’s Storage tree. Corresponds to
+ [`liveblocks.getStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ rooms-roomId-storage).
+
+ The default outputted format is called “plain LSON”, which includes information on the Live data
+ structures in the tree. These nodes show up in the output as objects with two properties, for
+ example:
+
+ ```json
+ {
+ \"liveblocksType\": \"LiveObject\",
+ \"data\": ...
+ }
+ ```
+
+ If you’re not interested in this information, you can use the simpler `?format=json` query param,
+ see below.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ format_ (GetStorageDocumentFormat | Unset): Use the `json` format to output a simplified
+ JSON representation of the Storage tree. In that format, each LiveObject and LiveMap will
+ be formatted as a simple JSON object, and each LiveList will be formatted as a simple JSON
+ array. This is a lossy format because information about the original data structures is
+ not retained, but it may be easier to work with. Example: json.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetStorageDocumentResponse
+ """
+
+ from .api.storage import get_storage_document
+
+ return await get_storage_document._asyncio(
+ room_id=room_id,
+ format_=format_,
+ client=self._client,
+ )
+
+ async def initialize_storage_document(
+ self,
+ room_id: str,
+ *,
+ body: InitializeStorageDocumentBody | Unset = UNSET,
+ ) -> InitializeStorageDocumentResponse:
+ r"""Initialize Storage document
+
+ This endpoint initializes or reinitializes a room’s Storage. The room must already exist. Calling
+ this endpoint will disconnect all users from the room if there are any, triggering a reconnect.
+ Corresponds to [`liveblocks.initializeStorageDocument`](https://liveblocks.io/docs/api-
+ reference/liveblocks-node#post-rooms-roomId-storage).
+
+ The format of the request body is the same as what’s returned by the get Storage endpoint.
+
+ For each Liveblocks data structure that you want to create, you need a JSON element having two
+ properties:
+ - `\"liveblocksType\"` => `\"LiveObject\" | \"LiveList\" | \"LiveMap\"`
+ - `\"data\"` => contains the nested data structures (children) and data.
+
+ The root’s type can only be LiveObject.
+
+ A utility function, `toPlainLson` is included in `@liveblocks/client` from `1.0.9` to help convert
+ `LiveObject`, `LiveList`, and `LiveMap` to the structure expected by the endpoint.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (InitializeStorageDocumentBody | Unset):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ InitializeStorageDocumentResponse
+ """
+
+ from .api.storage import initialize_storage_document
+
+ return await initialize_storage_document._asyncio(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def delete_storage_document(
+ self,
+ room_id: str,
+ ) -> None:
+ """Delete Storage document
+
+ This endpoint deletes all of the room’s Storage data. Calling this endpoint will disconnect all
+ users from the room if there are any. Corresponds to
+ [`liveblocks.deleteStorageDocument`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-rooms-roomId-storage).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.storage import delete_storage_document
+
+ return await delete_storage_document._asyncio(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ async def patch_storage_document(
+ self,
+ room_id: str,
+ *,
+ body: list[
+ AddJsonPatchOperation
+ | CopyJsonPatchOperation
+ | MoveJsonPatchOperation
+ | RemoveJsonPatchOperation
+ | ReplaceJsonPatchOperation
+ | TestJsonPatchOperation
+ ],
+ ) -> None:
+ """Apply JSON Patch to Storage
+
+ Applies a sequence of [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations to the
+ room's Storage document, useful for modifying Storage. Operations are applied in order; if any
+ operation fails, the document is not changed and a 422 response with a helpful message is returned.
+
+ **Paths and data types:** Be as specific as possible with your target path. Every parent in the
+ chain of path segments must be a LiveObject, LiveList, or LiveMap. Complex nested objects passed in
+ `add` or `replace` operations are automatically converted to LiveObjects and LiveLists.
+
+ **Performance:** For large Storage documents, applying a patch can be expensive because the full
+ state is reconstructed on the server to apply the operations. Very large documents may not be
+ suitable for this endpoint.
+
+ For a **full guide with examples**, see [Modifying storage via REST API with JSON
+ Patch](https://liveblocks.io/docs/guides/modifying-storage-via-rest-api-with-json-patch).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (list[AddJsonPatchOperation | CopyJsonPatchOperation | MoveJsonPatchOperation |
+ RemoveJsonPatchOperation | ReplaceJsonPatchOperation | TestJsonPatchOperation]): Example:
+ [{'op': 'add', 'path': '/score', 'value': 42}, {'op': 'remove', 'path': '/oldKey'}].
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.storage import patch_storage_document
+
+ return await patch_storage_document._asyncio(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_yjs_document(
+ self,
+ room_id: str,
+ *,
+ formatting: bool | Unset = UNSET,
+ key: str | Unset = UNSET,
+ type_: GetYjsDocumentType | Unset = UNSET,
+ ) -> GetYjsDocumentResponse:
+ """Get Yjs document
+
+ This endpoint returns a JSON representation of the room’s Yjs document. Corresponds to
+ [`liveblocks.getYjsDocument`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-
+ roomId-ydoc).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ formatting (bool | Unset): If present, YText will return formatting.
+ key (str | Unset): Returns only a single key’s value, e.g. `doc.get(key).toJSON()`.
+ Example: root.
+ type_ (GetYjsDocumentType | Unset): Used with key to override the inferred type, i.e.
+ `"ymap"` will return `doc.get(key, Y.Map)`. Example: ymap.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetYjsDocumentResponse
+ """
+
+ from .api.yjs import get_yjs_document
+
+ return await get_yjs_document._asyncio(
+ room_id=room_id,
+ formatting=formatting,
+ key=key,
+ type_=type_,
+ client=self._client,
+ )
+
+ async def send_yjs_binary_update(
+ self,
+ room_id: str,
+ *,
+ body: File,
+ guid: str | Unset = UNSET,
+ ) -> None:
+ """Send a binary Yjs update
+
+ This endpoint is used to send a Yjs binary update to the room’s Yjs document. You can use this
+ endpoint to initialize Yjs data for the room or to update the room’s Yjs document. To send an update
+ to a subdocument instead of the main document, pass its `guid`. Corresponds to
+ [`liveblocks.sendYjsBinaryUpdate`](https://liveblocks.io/docs/api-reference/liveblocks-node#put-
+ rooms-roomId-ydoc).
+
+ The update is typically obtained by calling `Y.encodeStateAsUpdate(doc)`. See the [Yjs
+ documentation](https://docs.yjs.dev/api/document-updates) for more details. When manually making
+ this HTTP call, set the HTTP header `Content-Type` to `application/octet-stream`, and send the
+ binary update (a `Uint8Array`) in the body of the HTTP request. This endpoint does not accept JSON,
+ unlike most other endpoints.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ guid (str | Unset): ID of the subdocument Example: subdoc-guid-123.
+ body (File):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.yjs import send_yjs_binary_update
+
+ return await send_yjs_binary_update._asyncio(
+ room_id=room_id,
+ body=body,
+ guid=guid,
+ client=self._client,
+ )
+
+ async def get_yjs_document_as_binary_update(
+ self,
+ room_id: str,
+ *,
+ guid: str | Unset = UNSET,
+ ) -> File:
+ """Get Yjs document encoded as a binary Yjs update
+
+ This endpoint returns the room's Yjs document encoded as a single binary update. This can be used by
+ `Y.applyUpdate(responseBody)` to get a copy of the document in your back end. See [Yjs
+ documentation](https://docs.yjs.dev/api/document-updates) for more information on working with
+ updates. To return a subdocument instead of the main document, pass its `guid`. Corresponds to
+ [`liveblocks.getYjsDocumentAsBinaryUpdate`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-rooms-roomId-ydoc-binary).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ guid (str | Unset): ID of the subdocument Example: subdoc-guid-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ File
+ """
+
+ from .api.yjs import get_yjs_document_as_binary_update
+
+ return await get_yjs_document_as_binary_update._asyncio(
+ room_id=room_id,
+ guid=guid,
+ client=self._client,
+ )
+
+ async def get_yjs_versions(
+ self,
+ room_id: str,
+ *,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+ ) -> GetYjsVersionsResponse:
+ """Get Yjs version history
+
+ This endpoint returns a list of version history snapshots for the room's Yjs document. The versions
+ are returned sorted by creation date, from newest to oldest.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ limit (int | Unset): A limit on the number of versions to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ cursor (str | Unset): A cursor used for pagination. Get the value from the `nextCursor`
+ response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetYjsVersionsResponse
+ """
+
+ from .api.yjs import get_yjs_versions
+
+ return await get_yjs_versions._asyncio(
+ room_id=room_id,
+ limit=limit,
+ cursor=cursor,
+ client=self._client,
+ )
+
+ async def get_yjs_version(
+ self,
+ room_id: str,
+ version_id: str,
+ ) -> File:
+ """Get Yjs document version
+
+ This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ version_id (str): ID of the version Example: vh_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ File
+ """
+
+ from .api.yjs import get_yjs_version
+
+ return await get_yjs_version._asyncio(
+ room_id=room_id,
+ version_id=version_id,
+ client=self._client,
+ )
+
+ async def create_yjs_version(
+ self,
+ room_id: str,
+ ) -> CreateYjsVersionResponse:
+ """Create Yjs version snapshot
+
+ This endpoint creates a new version history snapshot for the room's Yjs document.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ CreateYjsVersionResponse
+ """
+
+ from .api.yjs import create_yjs_version
+
+ return await create_yjs_version._asyncio(
+ room_id=room_id,
+ client=self._client,
+ )
+
+ async def get_threads(
+ self,
+ room_id: str,
+ *,
+ query: str | Unset = UNSET,
+ ) -> GetThreadsResponse:
+ """Get room threads
+
+ This endpoint returns the threads in the requested room. Corresponds to
+ [`liveblocks.getThreads`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-
+ threads).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ query (str | Unset): Query to filter threads. You can filter by `metadata` and `resolved`,
+ for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND resolved:true`.
+ Learn more about [filtering threads with query
+ language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language).
+ Example: metadata["color"]:"blue".
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetThreadsResponse
+ """
+
+ from .api.comments import get_threads
+
+ return await get_threads._asyncio(
+ room_id=room_id,
+ query=query,
+ client=self._client,
+ )
+
+ async def create_thread(
+ self,
+ room_id: str,
+ *,
+ body: CreateThreadRequestBody,
+ ) -> Thread:
+ r"""Create thread
+
+ This endpoint creates a new thread and the first comment in the thread. Corresponds to
+ [`liveblocks.createThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-threads).
+
+ A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to
+ construct a comment’s body, which can be submitted under `comment.body`.
+
+ ```json
+ {
+ \"version\": 1,
+ \"content\": [
+ {
+ \"type\": \"paragraph\",
+ \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]
+ }
+ ]
+ }
+ ```
+
+ `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value
+ length has a limit of 4000 characters maximum for strings.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (CreateThreadRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Thread
+ """
+
+ from .api.comments import create_thread
+
+ return await create_thread._asyncio(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_thread(
+ self,
+ room_id: str,
+ thread_id: str,
+ ) -> Thread:
+ """Get thread
+
+ This endpoint returns a thread by its ID. Corresponds to
+ [`liveblocks.getThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-
+ threads-threadId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Thread
+ """
+
+ from .api.comments import get_thread
+
+ return await get_thread._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ client=self._client,
+ )
+
+ async def delete_thread(
+ self,
+ room_id: str,
+ thread_id: str,
+ ) -> None:
+ """Delete thread
+
+ This endpoint deletes a thread by its ID. Corresponds to
+ [`liveblocks.deleteThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-
+ roomId-threads-threadId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.comments import delete_thread
+
+ return await delete_thread._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ client=self._client,
+ )
+
+ async def edit_thread_metadata(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: EditThreadMetadataRequestBody,
+ ) -> ThreadMetadata:
+ """Edit thread metadata
+
+ This endpoint edits the metadata of a thread. The metadata is a JSON object that can be used to
+ store any information you want about the thread, in `string`, `number`, or `boolean` form. Set a
+ property to `null` to remove it. Corresponds to
+ [`liveblocks.editThreadMetadata`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-metadata).
+
+ `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value
+ length has a limit of 4000 characters maximum for strings.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (EditThreadMetadataRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ThreadMetadata
+ """
+
+ from .api.comments import edit_thread_metadata
+
+ return await edit_thread_metadata._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def mark_thread_as_resolved(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: MarkThreadAsResolvedRequestBody,
+ ) -> Thread:
+ """Mark thread as resolved
+
+ This endpoint marks a thread as resolved. The request body must include a `userId` to identify who
+ resolved the thread. Returns the updated thread. Corresponds to
+ [`liveblocks.markThreadAsResolved`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-mark-as-resolved).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (MarkThreadAsResolvedRequestBody): Example: {'userId': 'alice'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Thread
+ """
+
+ from .api.comments import mark_thread_as_resolved
+
+ return await mark_thread_as_resolved._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def mark_thread_as_unresolved(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: MarkThreadAsUnresolvedRequestBody,
+ ) -> Thread:
+ """Mark thread as unresolved
+
+ This endpoint marks a thread as unresolved. The request body must include a `userId` to identify who
+ unresolved the thread. Returns the updated thread. Corresponds to
+ [`liveblocks.markThreadAsUnresolved`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-mark-as-unresolved).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (MarkThreadAsUnresolvedRequestBody): Example: {'userId': 'alice'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Thread
+ """
+
+ from .api.comments import mark_thread_as_unresolved
+
+ return await mark_thread_as_unresolved._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def subscribe_to_thread(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: SubscribeToThreadRequestBody,
+ ) -> Subscription:
+ """Subscribe to thread
+
+ This endpoint subscribes to a thread. Corresponds to
+ [`liveblocks.subscribeToThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-subscribe).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (SubscribeToThreadRequestBody): Example: {'userId': 'alice'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Subscription
+ """
+
+ from .api.comments import subscribe_to_thread
+
+ return await subscribe_to_thread._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def unsubscribe_from_thread(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: UnsubscribeFromThreadRequestBody,
+ ) -> None:
+ """Unsubscribe from thread
+
+ This endpoint unsubscribes from a thread. Corresponds to
+ [`liveblocks.unsubscribeFromThread`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-unsubscribe).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (UnsubscribeFromThreadRequestBody): Example: {'userId': 'alice'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.comments import unsubscribe_from_thread
+
+ return await unsubscribe_from_thread._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_thread_subscriptions(
+ self,
+ room_id: str,
+ thread_id: str,
+ ) -> GetThreadSubscriptionsResponse:
+ """Get thread subscriptions
+
+ This endpoint gets the list of subscriptions to a thread. Corresponds to
+ [`liveblocks.getThreadSubscriptions`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ rooms-roomId-threads-threadId-subscriptions).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetThreadSubscriptionsResponse
+ """
+
+ from .api.comments import get_thread_subscriptions
+
+ return await get_thread_subscriptions._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ client=self._client,
+ )
+
+ async def create_comment(
+ self,
+ room_id: str,
+ thread_id: str,
+ *,
+ body: CreateCommentRequestBody,
+ ) -> Comment:
+ r"""Create comment
+
+ This endpoint creates a new comment, adding it as a reply to a thread. Corresponds to
+ [`liveblocks.createComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-threads-threadId-comments).
+
+ A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to
+ construct a comment’s body, which can be submitted under `body`.
+
+ ```json
+ {
+ \"version\": 1,
+ \"content\": [
+ {
+ \"type\": \"paragraph\",
+ \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]
+ }
+ ]
+ }
+ ```
+
+ `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value
+ length has a limit of 4000 characters maximum for strings.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ body (CreateCommentRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Comment
+ """
+
+ from .api.comments import create_comment
+
+ return await create_comment._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_comment(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ ) -> Comment:
+ """Get comment
+
+ This endpoint returns a comment by its ID. Corresponds to
+ [`liveblocks.getComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-
+ threads-threadId-comments-commentId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Comment
+ """
+
+ from .api.comments import get_comment
+
+ return await get_comment._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ client=self._client,
+ )
+
+ async def edit_comment(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: EditCommentRequestBody,
+ ) -> Comment:
+ r"""Edit comment
+
+ This endpoint edits the specified comment. Corresponds to
+ [`liveblocks.editComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-threads-threadId-comments-commentId).
+
+ A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to
+ construct a comment’s body, which can be submitted under `body`.
+
+ ```json
+ {
+ \"version\": 1,
+ \"content\": [
+ {
+ \"type\": \"paragraph\",
+ \"children\": [{ \"text\": \"Hello \" }, { \"text\": \"world\", \"bold\": true }]
+ }
+ ]
+ }
+ ```
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+ body (EditCommentRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Comment
+ """
+
+ from .api.comments import edit_comment
+
+ return await edit_comment._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def delete_comment(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ ) -> None:
+ """Delete comment
+
+ This endpoint deletes a comment. A deleted comment is no longer accessible from the API or the
+ dashboard and it cannot be restored. Corresponds to
+ [`liveblocks.deleteComment`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-threads-threadId-comments-commentId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.comments import delete_comment
+
+ return await delete_comment._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ client=self._client,
+ )
+
+ async def add_comment_reaction(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: AddCommentReactionRequestBody,
+ ) -> CommentReaction:
+ """Add comment reaction
+
+ This endpoint adds a reaction to a comment. Corresponds to
+ [`liveblocks.addCommentReaction`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-comments-commentId-add-reaction).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+ body (AddCommentReactionRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ CommentReaction
+ """
+
+ from .api.comments import add_comment_reaction
+
+ return await add_comment_reaction._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def remove_comment_reaction(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: RemoveCommentReactionRequestBody | Unset = UNSET,
+ ) -> None:
+ """Remove comment reaction
+
+ This endpoint removes a comment reaction. A deleted comment reaction is no longer accessible from
+ the API or the dashboard and it cannot be restored. Corresponds to
+ [`liveblocks.removeCommentReaction`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-comments-commentId-add-reaction).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+ body (RemoveCommentReactionRequestBody | Unset):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.comments import remove_comment_reaction
+
+ return await remove_comment_reaction._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def edit_comment_metadata(
+ self,
+ room_id: str,
+ thread_id: str,
+ comment_id: str,
+ *,
+ body: EditCommentMetadataRequestBody,
+ ) -> CommentMetadata:
+ """Edit comment metadata
+
+ This endpoint edits the metadata of a comment. The metadata is a JSON object that can be used to
+ store any information you want about the comment, in `string`, `number`, or `boolean` form. Set a
+ property to `null` to remove it. Corresponds to
+ [`liveblocks.editCommentMetadata`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-threads-threadId-comments-commentId-metadata).
+
+ `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value
+ length has a limit of 4000 characters maximum for strings.
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ thread_id (str): ID of the thread Example: th_abc123.
+ comment_id (str): ID of the comment Example: cm_abc123.
+ body (EditCommentMetadataRequestBody):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ CommentMetadata
+ """
+
+ from .api.comments import edit_comment_metadata
+
+ return await edit_comment_metadata._asyncio(
+ room_id=room_id,
+ thread_id=thread_id,
+ comment_id=comment_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def authorize_user(
+ self,
+ *,
+ body: AuthorizeUserRequestBody,
+ ) -> AuthorizeUserResponse:
+ r"""Get access token with secret key
+
+ This endpoint lets your application server (your back end) obtain a token that one of its clients
+ (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own
+ application’s custom authentication endpoint. When making this request, you’ll have to use your
+ secret key.
+
+ **Important:** The difference with an [ID token](#post-identify-user) is that an access token holds
+ all the permissions, and is the source of truth. With ID tokens, permissions are set in the
+ Liveblocks back end (through REST API calls) and \"checked at the door\" every time they are used to
+ enter a room.
+
+ **Note:** When using the `@liveblocks/node` package, you can use
+ [`Liveblocks.prepareSession`](https://liveblocks.io/docs/api-reference/liveblocks-node#access-
+ tokens) in your back end to build this request.
+
+ You can pass the property `userId` in the request’s body. This can be whatever internal identifier
+ you use for your user accounts as long as it uniquely identifies an account. The property `userId`
+ is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId`
+ corresponds to one MAU.
+
+ Additionally, you can set custom metadata to the token, which will be publicly accessible by other
+ clients through the `user.info` property. This is useful for storing static data like avatar images
+ or the user’s display name.
+
+ Lastly, you’ll specify the exact permissions to give to the user using the `permissions` field. This
+ is done in an object where the keys are room names, or room name patterns (ending in a `*`), and a
+ list of permissions to assign the user for any room that matches that name exactly (or starts with
+ the pattern’s prefix). For tips, see [Manage permissions with access
+ tokens](https://liveblocks.io/docs/authentication/access-token).
+
+ Args:
+ body (AuthorizeUserRequestBody): Example: {'userId': 'user-123', 'userInfo': {'name':
+ 'bob', 'avatar': 'https://example.org/images/user123.jpg'}, 'organizationId': 'acme-corp',
+ 'permissions': {'my-room-1': ['room:write'], 'my-room-2': ['room:write'], 'my-room-*':
+ ['room:read']}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ AuthorizeUserResponse
+ """
+
+ from .api.auth import authorize_user
+
+ return await authorize_user._asyncio(
+ body=body,
+ client=self._client,
+ )
+
+ async def identify_user(
+ self,
+ *,
+ body: IdentifyUserRequestBody,
+ ) -> IdentifyUserResponse:
+ r"""Get ID token with secret key
+
+ This endpoint lets your application server (your back end) obtain a token that one of its clients
+ (your frontend) can use to enter a Liveblocks room. You use this endpoint to implement your own
+ application’s custom authentication endpoint. When using this endpoint to obtain ID tokens, you
+ should manage your permissions by assigning user and/or group permissions to rooms explicitly, see
+ our [Manage permissions with ID tokens](https://liveblocks.io/docs/authentication/id-token) section.
+
+ **Important:** The difference with an [access token](#post-authorize-user) is that an ID token
+ doesn’t hold any permissions itself. With ID tokens, permissions are set in the Liveblocks back end
+ (through REST API calls) and \"checked at the door\" every time they are used to enter a room. With
+ access tokens, all permissions are set in the token itself, and thus controlled from your back end
+ entirely.
+
+ **Note:** When using the `@liveblocks/node` package, you can use
+ [`Liveblocks.identifyUser`](https://liveblocks.io/docs/api-reference/liveblocks-node) in your back
+ end to build this request.
+
+ You can pass the property `userId` in the request’s body. This can be whatever internal identifier
+ you use for your user accounts as long as it uniquely identifies an account. The property `userId`
+ is used by Liveblocks to calculate your account’s Monthly Active Users. One unique `userId`
+ corresponds to one MAU.
+
+ If you want to use group permissions, you can also declare which `groupIds` this user belongs to.
+ The group ID values are yours, but they will have to match the group IDs you assign permissions to
+ when assigning permissions to rooms, see [Manage permissions with ID
+ tokens](https://liveblocks.io/docs/authentication/id-token)).
+
+ Additionally, you can set custom metadata to the token, which will be publicly accessible by other
+ clients through the `user.info` property. This is useful for storing static data like avatar images
+ or the user’s display name.
+
+ Args:
+ body (IdentifyUserRequestBody): Example: {'userId': 'user-123', 'organizationId': 'acme-
+ corp', 'groupIds': ['marketing', 'engineering'], 'userInfo': {'name': 'bob', 'avatar':
+ 'https://example.org/images/user123.jpg'}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ IdentifyUserResponse
+ """
+
+ from .api.auth import identify_user
+
+ return await identify_user._asyncio(
+ body=body,
+ client=self._client,
+ )
+
+ async def get_inbox_notification(
+ self,
+ user_id: str,
+ inbox_notification_id: str,
+ ) -> InboxNotificationCustomData | InboxNotificationThreadData:
+ """Get inbox notification
+
+ This endpoint returns a user’s inbox notification by its ID. Corresponds to
+ [`liveblocks.getInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ users-userId-inboxNotifications-inboxNotificationId).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+ inbox_notification_id (str): ID of the inbox notification Example: in_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ InboxNotificationCustomData | InboxNotificationThreadData
+ """
+
+ from .api.notifications import get_inbox_notification
+
+ return await get_inbox_notification._asyncio(
+ user_id=user_id,
+ inbox_notification_id=inbox_notification_id,
+ client=self._client,
+ )
+
+ async def delete_inbox_notification(
+ self,
+ user_id: str,
+ inbox_notification_id: str,
+ ) -> None:
+ """Delete inbox notification
+
+ This endpoint deletes a user’s inbox notification by its ID. Corresponds to
+ [`liveblocks.deleteInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-users-userId-inbox-notifications-inboxNotificationId).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+ inbox_notification_id (str): ID of the inbox notification Example: in_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.notifications import delete_inbox_notification
+
+ return await delete_inbox_notification._asyncio(
+ user_id=user_id,
+ inbox_notification_id=inbox_notification_id,
+ client=self._client,
+ )
+
+ async def get_inbox_notifications(
+ self,
+ user_id: str,
+ *,
+ organization_id: str | Unset = UNSET,
+ query: str | Unset = UNSET,
+ limit: int | Unset = 50,
+ starting_after: str | Unset = UNSET,
+ ) -> GetInboxNotificationsResponse:
+ """Get all inbox notifications
+
+ This endpoint returns all the user’s inbox notifications. Corresponds to
+ [`liveblocks.getInboxNotifications`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ users-userId-inboxNotifications).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+ organization_id (str | Unset): The organization ID to filter notifications for. Example:
+ org_123456789.
+ query (str | Unset): Query to filter notifications. You can filter by `unread`, for
+ example, `unread:true`. Example: metadata["color"]:"blue".
+ limit (int | Unset): A limit on the number of inbox notifications to be returned. The
+ limit can range between 1 and 50, and defaults to 50. Default: 50. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetInboxNotificationsResponse
+ """
+
+ from .api.notifications import get_inbox_notifications
+
+ return await get_inbox_notifications._asyncio(
+ user_id=user_id,
+ organization_id=organization_id,
+ query=query,
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ async def delete_all_inbox_notifications(
+ self,
+ user_id: str,
+ ) -> None:
+ """Delete all inbox notifications
+
+ This endpoint deletes all the user’s inbox notifications. Corresponds to
+ [`liveblocks.deleteAllInboxNotifications`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-users-userId-inbox-notifications).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.notifications import delete_all_inbox_notifications
+
+ return await delete_all_inbox_notifications._asyncio(
+ user_id=user_id,
+ client=self._client,
+ )
+
+ async def get_notification_settings(
+ self,
+ user_id: str,
+ ) -> NotificationSettings:
+ """Get notification settings
+
+ This endpoint returns a user's notification settings for the project. Corresponds to
+ [`liveblocks.getNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ users-userId-notification-settings).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ NotificationSettings
+ """
+
+ from .api.notifications import get_notification_settings
+
+ return await get_notification_settings._asyncio(
+ user_id=user_id,
+ client=self._client,
+ )
+
+ async def update_notification_settings(
+ self,
+ user_id: str,
+ *,
+ body: UpdateNotificationSettingsRequestBody,
+ ) -> NotificationSettings:
+ """Update notification settings
+
+ This endpoint updates a user's notification settings for the project. Corresponds to
+ [`liveblocks.updateNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#post-users-userId-notification-settings).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+ body (UpdateNotificationSettingsRequestBody): Partial notification settings - all
+ properties are optional Example: {'email': {'thread': True, 'textMention': False},
+ 'slack': {'textMention': False}, 'webPush': {'thread': True}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ NotificationSettings
+ """
+
+ from .api.notifications import update_notification_settings
+
+ return await update_notification_settings._asyncio(
+ user_id=user_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def delete_notification_settings(
+ self,
+ user_id: str,
+ ) -> None:
+ """Delete notification settings
+
+ This endpoint deletes a user's notification settings for the project. Corresponds to
+ [`liveblocks.deleteNotificationSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-users-userId-notification-settings).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.notifications import delete_notification_settings
+
+ return await delete_notification_settings._asyncio(
+ user_id=user_id,
+ client=self._client,
+ )
+
+ async def get_room_subscription_settings(
+ self,
+ room_id: str,
+ user_id: str,
+ ) -> RoomSubscriptionSettings:
+ """Get room subscription settings
+
+ This endpoint returns a user’s subscription settings for a specific room. Corresponds to
+ [`liveblocks.getRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-rooms-roomId-users-userId-subscription-settings).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ user_id (str): ID of the user Example: user-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ RoomSubscriptionSettings
+ """
+
+ from .api.notifications import get_room_subscription_settings
+
+ return await get_room_subscription_settings._asyncio(
+ room_id=room_id,
+ user_id=user_id,
+ client=self._client,
+ )
+
+ async def update_room_subscription_settings(
+ self,
+ room_id: str,
+ user_id: str,
+ *,
+ body: UpdateRoomSubscriptionSettingsRequestBody,
+ ) -> RoomSubscriptionSettings:
+ """Update room subscription settings
+
+ This endpoint updates a user’s subscription settings for a specific room. Corresponds to
+ [`liveblocks.updateRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#post-rooms-roomId-users-userId-subscription-settings).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ user_id (str): ID of the user Example: user-123.
+ body (UpdateRoomSubscriptionSettingsRequestBody): Partial room subscription settings - all
+ properties are optional Example: {'threads': 'replies_and_mentions', 'textMentions':
+ 'none'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ RoomSubscriptionSettings
+ """
+
+ from .api.notifications import update_room_subscription_settings
+
+ return await update_room_subscription_settings._asyncio(
+ room_id=room_id,
+ user_id=user_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def delete_room_subscription_settings(
+ self,
+ room_id: str,
+ user_id: str,
+ ) -> None:
+ """Delete room subscription settings
+
+ This endpoint deletes a user’s subscription settings for a specific room. Corresponds to
+ [`liveblocks.deleteRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-rooms-roomId-users-userId-subscription-settings).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ user_id (str): ID of the user Example: user-123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.notifications import delete_room_subscription_settings
+
+ return await delete_room_subscription_settings._asyncio(
+ room_id=room_id,
+ user_id=user_id,
+ client=self._client,
+ )
+
+ async def get_user_room_subscription_settings(
+ self,
+ user_id: str,
+ *,
+ starting_after: str | Unset = UNSET,
+ limit: int | Unset = 50,
+ organization_id: str | Unset = UNSET,
+ ) -> GetRoomSubscriptionSettingsResponse:
+ """Get user room subscription settings
+
+ This endpoint returns the list of a user's room subscription settings. Corresponds to
+ [`liveblocks.getUserRoomSubscriptionSettings`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-users-userId-room-subscription-settings).
+
+ Args:
+ user_id (str): ID of the user Example: user-123.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+ limit (int | Unset): A limit on the number of elements to be returned. The limit can range
+ between 1 and 50, and defaults to 50. Default: 50. Example: 20.
+ organization_id (str | Unset): The organization ID to filter room subscription settings
+ for. Example: org_123456789.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetRoomSubscriptionSettingsResponse
+ """
+
+ from .api.notifications import get_user_room_subscription_settings
+
+ return await get_user_room_subscription_settings._asyncio(
+ user_id=user_id,
+ starting_after=starting_after,
+ limit=limit,
+ organization_id=organization_id,
+ client=self._client,
+ )
+
+ async def trigger_inbox_notification(
+ self,
+ *,
+ body: TriggerInboxNotificationRequestBody | Unset = UNSET,
+ ) -> None:
+ """Trigger inbox notification
+
+ This endpoint triggers an inbox notification. Corresponds to
+ [`liveblocks.triggerInboxNotification`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#post-inbox-notifications-trigger).
+
+ Args:
+ body (TriggerInboxNotificationRequestBody | Unset): Example: {'userId': 'alice', 'kind':
+ 'file-uploaded', 'subjectId': 'file123', 'activityData': {'url': 'url-to-file'}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.notifications import trigger_inbox_notification
+
+ return await trigger_inbox_notification._asyncio(
+ body=body,
+ client=self._client,
+ )
+
+ async def get_groups(
+ self,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ ) -> GetGroupsResponse:
+ """Get groups
+
+ This endpoint returns a list of all groups in your project. Corresponds to
+ [`liveblocks.getGroups`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-groups).
+
+ Args:
+ limit (int | Unset): A limit on the number of groups to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetGroupsResponse
+ """
+
+ from .api.groups import get_groups
+
+ return await get_groups._asyncio(
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ async def create_group(
+ self,
+ *,
+ body: CreateGroupRequestBody | Unset = UNSET,
+ ) -> Group:
+ """Create group
+
+ This endpoint creates a new group. Corresponds to
+ [`liveblocks.createGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-group).
+
+ Args:
+ body (CreateGroupRequestBody | Unset): Example: {'id': 'engineering', 'memberIds':
+ ['alice', 'bob'], 'organizationId': 'org_123456789', 'scopes': {'mention': True}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Group
+ """
+
+ from .api.groups import create_group
+
+ return await create_group._asyncio(
+ body=body,
+ client=self._client,
+ )
+
+ async def get_group(
+ self,
+ group_id: str,
+ ) -> Group:
+ """Get group
+
+ This endpoint returns a specific group by ID. Corresponds to
+ [`liveblocks.getGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-group).
+
+ Args:
+ group_id (str): The ID of the group to retrieve. Example: engineering.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Group
+ """
+
+ from .api.groups import get_group
+
+ return await get_group._asyncio(
+ group_id=group_id,
+ client=self._client,
+ )
+
+ async def delete_group(
+ self,
+ group_id: str,
+ ) -> None:
+ """Delete group
+
+ This endpoint deletes a group. Corresponds to
+ [`liveblocks.deleteGroup`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-group).
+
+ Args:
+ group_id (str): The ID of the group to delete. Example: engineering.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.groups import delete_group
+
+ return await delete_group._asyncio(
+ group_id=group_id,
+ client=self._client,
+ )
+
+ async def add_group_members(
+ self,
+ group_id: str,
+ *,
+ body: AddGroupMembersRequestBody,
+ ) -> Group:
+ """Add group members
+
+ This endpoint adds new members to an existing group. Corresponds to
+ [`liveblocks.addGroupMembers`](https://liveblocks.io/docs/api-reference/liveblocks-node#add-group-
+ members).
+
+ Args:
+ group_id (str): The ID of the group to add members to. Example: engineering.
+ body (AddGroupMembersRequestBody): Example: {'memberIds': ['charlie', 'dave']}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Group
+ """
+
+ from .api.groups import add_group_members
+
+ return await add_group_members._asyncio(
+ group_id=group_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def remove_group_members(
+ self,
+ group_id: str,
+ *,
+ body: RemoveGroupMembersRequestBody,
+ ) -> Group:
+ """Remove group members
+
+ This endpoint removes members from an existing group. Corresponds to
+ [`liveblocks.removeGroupMembers`](https://liveblocks.io/docs/api-reference/liveblocks-node#remove-
+ group-members).
+
+ Args:
+ group_id (str): The ID of the group to remove members from. Example: engineering.
+ body (RemoveGroupMembersRequestBody): Example: {'memberIds': ['charlie']}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ Group
+ """
+
+ from .api.groups import remove_group_members
+
+ return await remove_group_members._asyncio(
+ group_id=group_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_user_groups(
+ self,
+ user_id: str,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ ) -> GetUserGroupsResponse:
+ """Get user groups
+
+ This endpoint returns all groups that a specific user is a member of. Corresponds to
+ [`liveblocks.getUserGroups`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-user-
+ groups).
+
+ Args:
+ user_id (str): The ID of the user to get groups for. Example: user-123.
+ limit (int | Unset): A limit on the number of groups to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetUserGroupsResponse
+ """
+
+ from .api.groups import get_user_groups
+
+ return await get_user_groups._asyncio(
+ user_id=user_id,
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ async def get_ai_copilots(
+ self,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ ) -> GetAiCopilotsResponse:
+ """Get AI copilots
+
+ This endpoint returns a paginated list of AI copilots. The copilots are returned sorted by creation
+ date, from newest to oldest. Corresponds to
+ [`liveblocks.getAiCopilots`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-ai-
+ copilots).
+
+ Args:
+ limit (int | Unset): A limit on the number of copilots to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetAiCopilotsResponse
+ """
+
+ from .api.ai import get_ai_copilots
+
+ return await get_ai_copilots._asyncio(
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ async def create_ai_copilot(
+ self,
+ *,
+ body: CreateAiCopilotOptionsAnthropic
+ | CreateAiCopilotOptionsGoogle
+ | CreateAiCopilotOptionsOpenAi
+ | CreateAiCopilotOptionsOpenAiCompatible,
+ ) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ """Create AI copilot
+
+ This endpoint creates a new AI copilot with the given configuration. Corresponds to
+ [`liveblocks.createAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-ai-
+ copilot).
+
+ Args:
+ body (CreateAiCopilotOptionsAnthropic | CreateAiCopilotOptionsGoogle |
+ CreateAiCopilotOptionsOpenAi | CreateAiCopilotOptionsOpenAiCompatible): Example: {'name':
+ 'My Copilot', 'systemPrompt': 'You are a helpful assistant.', 'providerApiKey': 'sk-...',
+ 'provider': 'openai', 'providerModel': 'gpt-4o'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible
+ """
+
+ from .api.ai import create_ai_copilot
+
+ return await create_ai_copilot._asyncio(
+ body=body,
+ client=self._client,
+ )
+
+ async def get_ai_copilot(
+ self,
+ copilot_id: str,
+ ) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ """Get AI copilot
+
+ This endpoint returns an AI copilot by its ID. Corresponds to
+ [`liveblocks.getAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-ai-
+ copilot).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible
+ """
+
+ from .api.ai import get_ai_copilot
+
+ return await get_ai_copilot._asyncio(
+ copilot_id=copilot_id,
+ client=self._client,
+ )
+
+ async def update_ai_copilot(
+ self,
+ copilot_id: str,
+ *,
+ body: UpdateAiCopilotRequestBody,
+ ) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ r"""Update AI copilot
+
+ This endpoint updates an existing AI copilot's configuration. Corresponds to
+ [`liveblocks.updateAiCopilot`](https://liveblocks.io/docs/api-reference/liveblocks-node#update-ai-
+ copilot).
+
+ This endpoint returns a 422 response if the update doesn't apply due to validation failures. For
+ example, if the existing copilot uses the \"openai\" provider and you attempt to update the provider
+ model to an incompatible value for the provider, like \"gemini-2.5-pro\", you'll receive a 422
+ response with an error message explaining where the validation failed.
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ body (UpdateAiCopilotRequestBody): Example: {'name': 'Updated Copilot', 'systemPrompt':
+ 'You are an updated helpful assistant.', 'providerModel': 'gpt-4o', 'settings':
+ {'maxTokens': 8192}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible
+ """
+
+ from .api.ai import update_ai_copilot
+
+ return await update_ai_copilot._asyncio(
+ copilot_id=copilot_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def delete_ai_copilot(
+ self,
+ copilot_id: str,
+ ) -> None:
+ """Delete AI copilot
+
+ This endpoint deletes an AI copilot by its ID. A deleted copilot is no longer accessible and cannot
+ be restored. Corresponds to [`liveblocks.deleteAiCopilot`](https://liveblocks.io/docs/api-
+ reference/liveblocks-node#delete-ai-copilot).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.ai import delete_ai_copilot
+
+ return await delete_ai_copilot._asyncio(
+ copilot_id=copilot_id,
+ client=self._client,
+ )
+
+ async def get_knowledge_sources(
+ self,
+ copilot_id: str,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ ) -> GetKnowledgeSourcesResponse:
+ """Get knowledge sources
+
+ This endpoint returns a paginated list of knowledge sources for a specific AI copilot. Corresponds
+ to [`liveblocks.getKnowledgeSources`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ knowledge-sources).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ limit (int | Unset): A limit on the number of knowledge sources to be returned. The limit
+ can range between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetKnowledgeSourcesResponse
+ """
+
+ from .api.ai import get_knowledge_sources
+
+ return await get_knowledge_sources._asyncio(
+ copilot_id=copilot_id,
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ async def get_knowledge_source(
+ self,
+ copilot_id: str,
+ knowledge_source_id: str,
+ ) -> KnowledgeSourceFileSource | KnowledgeSourceWebSource:
+ """Get knowledge source
+
+ This endpoint returns a specific knowledge source by its ID. Corresponds to
+ [`liveblocks.getKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-
+ knowledge-source).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ knowledge_source_id (str): ID of the knowledge source Example: ks_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ KnowledgeSourceFileSource | KnowledgeSourceWebSource
+ """
+
+ from .api.ai import get_knowledge_source
+
+ return await get_knowledge_source._asyncio(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ client=self._client,
+ )
+
+ async def create_web_knowledge_source(
+ self,
+ copilot_id: str,
+ *,
+ body: CreateWebKnowledgeSourceRequestBody,
+ ) -> CreateWebKnowledgeSourceResponse:
+ """Create web knowledge source
+
+ This endpoint creates a web knowledge source for an AI copilot. This allows the copilot to access
+ and learn from web content. Corresponds to
+ [`liveblocks.createWebKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#create-web-knowledge-source).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ body (CreateWebKnowledgeSourceRequestBody): Example: {'copilotId': 'cp_abc123', 'url':
+ 'https://docs.example.com', 'type': 'crawl'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ CreateWebKnowledgeSourceResponse
+ """
+
+ from .api.ai import create_web_knowledge_source
+
+ return await create_web_knowledge_source._asyncio(
+ copilot_id=copilot_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def create_file_knowledge_source(
+ self,
+ copilot_id: str,
+ name: str,
+ *,
+ body: File,
+ ) -> CreateFileKnowledgeSourceResponse200:
+ """Create file knowledge source
+
+ This endpoint creates a file knowledge source for an AI copilot by uploading a file. The copilot can
+ then reference the content of the file when responding. Corresponds to
+ [`liveblocks.createFileKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#create-file-knowledge-source).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ name (str): Name of the file Example: document.pdf.
+ body (File):
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ CreateFileKnowledgeSourceResponse200
+ """
+
+ from .api.ai import create_file_knowledge_source
+
+ return await create_file_knowledge_source._asyncio(
+ copilot_id=copilot_id,
+ name=name,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_file_knowledge_source_markdown(
+ self,
+ copilot_id: str,
+ knowledge_source_id: str,
+ ) -> GetFileKnowledgeSourceMarkdownResponse:
+ """Get file knowledge source content
+
+ This endpoint returns the content of a file knowledge source as markdown. This allows you to see
+ what content the AI copilot has access to from uploaded files. Corresponds to
+ [`liveblocks.getFileKnowledgeSourceMarkdown`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-file-knowledge-source-markdown).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ knowledge_source_id (str): ID of the knowledge source Example: ks_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetFileKnowledgeSourceMarkdownResponse
+ """
+
+ from .api.ai import get_file_knowledge_source_markdown
+
+ return await get_file_knowledge_source_markdown._asyncio(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ client=self._client,
+ )
+
+ async def delete_file_knowledge_source(
+ self,
+ copilot_id: str,
+ knowledge_source_id: str,
+ ) -> None:
+ """Delete file knowledge source
+
+ This endpoint deletes a file knowledge source from an AI copilot. The copilot will no longer have
+ access to the content from this file. Corresponds to
+ [`liveblocks.deleteFileKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-file-knowledge-source).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ knowledge_source_id (str): ID of the knowledge source Example: ks_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.ai import delete_file_knowledge_source
+
+ return await delete_file_knowledge_source._asyncio(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ client=self._client,
+ )
+
+ async def delete_web_knowledge_source(
+ self,
+ copilot_id: str,
+ knowledge_source_id: str,
+ ) -> None:
+ """Delete web knowledge source
+
+ This endpoint deletes a web knowledge source from an AI copilot. The copilot will no longer have
+ access to the content from this source. Corresponds to
+ [`liveblocks.deleteWebKnowledgeSource`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#delete-web-knowledge-source).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ knowledge_source_id (str): ID of the knowledge source Example: ks_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.ai import delete_web_knowledge_source
+
+ return await delete_web_knowledge_source._asyncio(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ client=self._client,
+ )
+
+ async def get_web_knowledge_source_links(
+ self,
+ copilot_id: str,
+ knowledge_source_id: str,
+ *,
+ limit: int | Unset = 20,
+ starting_after: str | Unset = UNSET,
+ ) -> GetWebKnowledgeSourceLinksResponse:
+ """Get web knowledge source links
+
+ This endpoint returns a paginated list of links that were indexed from a web knowledge source. This
+ is useful for understanding what content the AI copilot has access to from web sources. Corresponds
+ to [`liveblocks.getWebKnowledgeSourceLinks`](https://liveblocks.io/docs/api-reference/liveblocks-
+ node#get-web-knowledge-source-links).
+
+ Args:
+ copilot_id (str): ID of the AI copilot Example: cp_abc123.
+ knowledge_source_id (str): ID of the knowledge source Example: ks_abc123.
+ limit (int | Unset): A limit on the number of links to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ starting_after (str | Unset): A cursor used for pagination. Get the value from the
+ `nextCursor` response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetWebKnowledgeSourceLinksResponse
+ """
+
+ from .api.ai import get_web_knowledge_source_links
+
+ return await get_web_knowledge_source_links._asyncio(
+ copilot_id=copilot_id,
+ knowledge_source_id=knowledge_source_id,
+ limit=limit,
+ starting_after=starting_after,
+ client=self._client,
+ )
+
+ async def get_management_projects(
+ self,
+ *,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+ ) -> GetManagementProjectsResponse:
+ """List projects
+
+ Returns a paginated list of projects. You can limit the number of projects returned per page and use
+ the provided `nextCursor` for pagination. This endpoint requires the `read:all` scope.
+
+ Args:
+ limit (int | Unset): A limit on the number of projects to return. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ cursor (str | Unset): A cursor used for pagination. Get the value from the `nextCursor`
+ response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetManagementProjectsResponse
+ """
+
+ from .api.management import get_management_projects
+
+ return await get_management_projects._asyncio(
+ limit=limit,
+ cursor=cursor,
+ client=self._client,
+ )
+
+ async def create_management_project(
+ self,
+ *,
+ body: CreateManagementProjectRequestBody,
+ ) -> ManagementProject:
+ """Create project
+
+ Creates a new project within your account. This endpoint requires the `write:all` scope. You can
+ specify the project type, name, and version creation timeout. Upon success, returns information
+ about the newly created project, including its ID, keys, region, and settings.
+
+ Args:
+ body (CreateManagementProjectRequestBody): Example: {'name': 'My Project', 'type': 'dev',
+ 'versionCreationTimeout': False}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementProject
+ """
+
+ from .api.management import create_management_project
+
+ return await create_management_project._asyncio(
+ body=body,
+ client=self._client,
+ )
+
+ async def get_management_project(
+ self,
+ project_id: str,
+ ) -> ManagementProject:
+ """Get project
+
+ Returns a single project specified by its ID. This endpoint requires the `read:all` scope. If the
+ project cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementProject
+ """
+
+ from .api.management import get_management_project
+
+ return await get_management_project._asyncio(
+ project_id=project_id,
+ client=self._client,
+ )
+
+ async def update_management_project(
+ self,
+ project_id: str,
+ *,
+ body: UpdateManagementProjectRequestBody,
+ ) -> ManagementProject:
+ """Update project
+
+ Updates an existing project specified by its ID. This endpoint allows you to modify project details
+ such as the project name and the version creation timeout. The `versionCreationTimeout` can be set
+ to `false` to disable the timeout or to a number of seconds between 30 and 300. Fields omitted from
+ the request body will not be updated. Requires the `write:all` scope.
+
+ If the project cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ body (UpdateManagementProjectRequestBody): Example: {'name': 'Updated Project Name',
+ 'versionCreationTimeout': 60}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementProject
+ """
+
+ from .api.management import update_management_project
+
+ return await update_management_project._asyncio(
+ project_id=project_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def delete_management_project(
+ self,
+ project_id: str,
+ ) -> None:
+ """Delete project
+
+ Soft deletes the project specified by its ID. This endpoint requires the `write:all` scope. If the
+ project cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.management import delete_management_project
+
+ return await delete_management_project._asyncio(
+ project_id=project_id,
+ client=self._client,
+ )
+
+ async def activate_project_public_api_key(
+ self,
+ project_id: str,
+ ) -> None:
+ """Activate public key
+
+ Activates the public API key associated with the specified project. This endpoint requires the
+ `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.management import activate_project_public_api_key
+
+ return await activate_project_public_api_key._asyncio(
+ project_id=project_id,
+ client=self._client,
+ )
+
+ async def deactivate_project_public_api_key(
+ self,
+ project_id: str,
+ ) -> None:
+ """Deactivate public key
+
+ Deactivates the public API key associated with the specified project. This endpoint requires the
+ `write:all` scope. If the project cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.management import deactivate_project_public_api_key
+
+ return await deactivate_project_public_api_key._asyncio(
+ project_id=project_id,
+ client=self._client,
+ )
+
+ async def roll_project_public_api_key(
+ self,
+ project_id: str,
+ *,
+ body: RollProjectPublicApiKeyRequestBody | Unset = UNSET,
+ ) -> RollProjectPublicApiKeyResponse:
+ """Roll public key
+
+ Rolls (rotates) the public API key associated with the specified project, generating a new key value
+ while deprecating the previous one. The new key becomes immediately active. This endpoint requires
+ the `write:all` scope.
+
+ If the public key is not currently enabled for the project, a 403 error response is returned. If the
+ project cannot be found, a 404 error response is returned. An optional `expirationIn` parameter can
+ be provided in the request body to set when the previous key should expire.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ body (RollProjectPublicApiKeyRequestBody | Unset): Example: {'expirationIn': '1 hour'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ RollProjectPublicApiKeyResponse
+ """
+
+ from .api.management import roll_project_public_api_key
+
+ return await roll_project_public_api_key._asyncio(
+ project_id=project_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def roll_project_secret_api_key(
+ self,
+ project_id: str,
+ *,
+ body: RollProjectSecretApiKeyRequestBody | Unset = UNSET,
+ ) -> ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse:
+ """Roll secret key
+
+ Rolls (rotates) the secret API key associated with the specified project, generating a new key value
+ while deprecating the previous one. The new key becomes immediately active. This endpoint requires
+ the `write:all` scope.
+
+ If the project cannot be found, a 404 error response is returned. An optional `expirationIn`
+ parameter can be provided in the request body to set when the previous key should expire.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ body (RollProjectSecretApiKeyRequestBody | Unset): Example: {'expirationIn': '3 days'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse
+ """
+
+ from .api.management import roll_project_secret_api_key
+
+ return await roll_project_secret_api_key._asyncio(
+ project_id=project_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_management_webhooks(
+ self,
+ project_id: str,
+ *,
+ limit: int | Unset = 20,
+ cursor: str | Unset = UNSET,
+ ) -> GetManagementWebhooksResponse:
+ """List webhooks
+
+ Returns a paginated list of webhooks for a project. This endpoint requires the `read:all` scope. The
+ response includes an array of webhook objects associated with the specified project, as well as a
+ `nextCursor` property for pagination. Use the `limit` query parameter to specify the maximum number
+ of webhooks to return (1-100, default 20). If the result is paginated, use the `cursor` parameter
+ from the `nextCursor` value in the previous response to fetch subsequent pages. If the project
+ cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ limit (int | Unset): A limit on the number of webhooks to return. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+ cursor (str | Unset): A cursor used for pagination. Get the value from the `nextCursor`
+ response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetManagementWebhooksResponse
+ """
+
+ from .api.management import get_management_webhooks
+
+ return await get_management_webhooks._asyncio(
+ project_id=project_id,
+ limit=limit,
+ cursor=cursor,
+ client=self._client,
+ )
+
+ async def create_management_webhook(
+ self,
+ project_id: str,
+ *,
+ body: CreateManagementWebhookRequestBody,
+ ) -> ManagementWebhook:
+ """Create webhook
+
+ Creates a new webhook for a project. This endpoint requires the `write:all` scope. If the project
+ cannot be found, a 404 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ body (CreateManagementWebhookRequestBody): Example: {'url':
+ 'https://example.com/webhooks', 'subscribedEvents': ['storageUpdated', 'userEntered'],
+ 'rateLimit': 100, 'storageUpdatedThrottleSeconds': 10, 'yDocUpdatedThrottleSeconds': 10}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementWebhook
+ """
+
+ from .api.management import create_management_webhook
+
+ return await create_management_webhook._asyncio(
+ project_id=project_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_management_webhook(
+ self,
+ project_id: str,
+ webhook_id: str,
+ ) -> ManagementWebhook:
+ """Get webhook
+
+ Get one webhook by `webhookId` for a project. Returns webhook settings such as URL, subscribed
+ events, disabled state, throttling, and additional headers. Returns `404` if the project or webhook
+ does not exist. This endpoint requires the `read:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementWebhook
+ """
+
+ from .api.management import get_management_webhook
+
+ return await get_management_webhook._asyncio(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ client=self._client,
+ )
+
+ async def update_management_webhook(
+ self,
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: UpdateManagementWebhookRequestBody,
+ ) -> ManagementWebhook:
+ """Update webhook
+
+ Update one webhook by `webhookId` for a project. Send only fields you want to change; omitted fields
+ stay unchanged. Returns `404` if the project or webhook does not exist and `422` for validation
+ errors. This endpoint requires the `write:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+ body (UpdateManagementWebhookRequestBody): Example: {'url':
+ 'https://example.com/webhooks', 'subscribedEvents': ['storageUpdated', 'userEntered'],
+ 'rateLimit': 100, 'disabled': False}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ ManagementWebhook
+ """
+
+ from .api.management import update_management_webhook
+
+ return await update_management_webhook._asyncio(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def delete_management_webhook(
+ self,
+ project_id: str,
+ webhook_id: str,
+ ) -> None:
+ """Delete webhook
+
+ Delete one webhook by `webhookId` for a project. Returns `200` with an empty body on success, or
+ `404` if the project or webhook does not exist. Requires `write:all`.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.management import delete_management_webhook
+
+ return await delete_management_webhook._asyncio(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ client=self._client,
+ )
+
+ async def roll_management_webhook_secret(
+ self,
+ project_id: str,
+ webhook_id: str,
+ ) -> RotateManagementWebhookSecretResponse:
+ """Roll webhook secret
+
+ Rotate a webhook signing secret and return the new secret. The previous secret remains valid for 24
+ hours. Returns `404` if the project or webhook does not exist. This endpoint requires the
+ `write:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ RotateManagementWebhookSecretResponse
+ """
+
+ from .api.management import roll_management_webhook_secret
+
+ return await roll_management_webhook_secret._asyncio(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ client=self._client,
+ )
+
+ async def get_management_webhook_additional_headers(
+ self,
+ project_id: str,
+ webhook_id: str,
+ ) -> GetManagementWebhookHeadersResponse:
+ """Get webhook headers
+
+ Get a webhook's additional headers. Returns `404` if the project or webhook does not exist. Requires
+ `read:all`.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ GetManagementWebhookHeadersResponse
+ """
+
+ from .api.management import get_management_webhook_additional_headers
+
+ return await get_management_webhook_additional_headers._asyncio(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ client=self._client,
+ )
+
+ async def upsert_management_webhook_additional_headers(
+ self,
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: UpsertManagementWebhookHeadersRequestBody,
+ ) -> UpsertManagementWebhookHeadersResponse:
+ """Patch webhook headers
+
+ Upsert additional headers for a webhook. Provided headers are merged with existing headers, and
+ existing values are overwritten when names match. Returns updated headers, or `404` if the project
+ or webhook does not exist. This endpoint requires the `write:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+ body (UpsertManagementWebhookHeadersRequestBody): Example: {'headers': {'X-Custom-
+ Header': 'value'}}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ UpsertManagementWebhookHeadersResponse
+ """
+
+ from .api.management import upsert_management_webhook_additional_headers
+
+ return await upsert_management_webhook_additional_headers._asyncio(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def delete_management_webhook_additional_headers(
+ self,
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: DeleteManagementWebhookHeadersRequestBody,
+ ) -> DeleteManagementWebhookHeadersResponse:
+ """Delete webhook headers
+
+ Remove selected additional headers from a webhook. Send header names in `headers` field; other
+ headers are unchanged. Returns updated headers, or `404` if the project or webhook does not exist.
+ This endpoint requires the `write:all` scope. At least one header name must be provided; otherwise,
+ a 422 error response is returned.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+ body (DeleteManagementWebhookHeadersRequestBody): Example: {'headers': ['X-Custom-
+ Header', 'X-Another-Header']}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ DeleteManagementWebhookHeadersResponse
+ """
+
+ from .api.management import delete_management_webhook_additional_headers
+
+ return await delete_management_webhook_additional_headers._asyncio(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def recover_failed_webhook_messages(
+ self,
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: RecoverManagementWebhookFailedMessagesRequestBody,
+ ) -> None:
+ """Recover failed webhook messages
+
+ Requeue failed deliveries for a webhook from the given `since` timestamp. Returns `200` with an
+ empty body when recovery starts, an `404` if the project or webhook does not exist. This endpoint
+ requires the `write:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+ body (RecoverManagementWebhookFailedMessagesRequestBody): Example: {'since':
+ '2026-01-21T00:00:00.000Z'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ None
+ """
+
+ from .api.management import recover_failed_webhook_messages
+
+ return await recover_failed_webhook_messages._asyncio(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def send_test_webhook(
+ self,
+ project_id: str,
+ webhook_id: str,
+ *,
+ body: TestManagementWebhookRequestBody,
+ ) -> TestManagementWebhookResponse:
+ """Send test webhook
+
+ Send a test event to a webhook and return the created message metadata. `subscribedEvent` must be
+ one of the webhook's subscribed events, otherwise the endpoint returns `422`. Returns `404` if the
+ project or webhook does not exist. This endpoint requires the `write:all` scope.
+
+ Args:
+ project_id (str): ID of the project Example: 683d49ed6b4d1cec5a597b13.
+ webhook_id (str): ID of the webhook Example: wh_abc123.
+ body (TestManagementWebhookRequestBody): Example: {'subscribedEvent': 'storageUpdated'}.
+
+ Raises:
+ errors.LiveblocksError: If the server returns a response with non-2xx status code.
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
+
+ Returns:
+ TestManagementWebhookResponse
+ """
+
+ from .api.management import send_test_webhook
+
+ return await send_test_webhook._asyncio(
+ project_id=project_id,
+ webhook_id=webhook_id,
+ body=body,
+ client=self._client,
+ )
diff --git a/packages/liveblocks-python/liveblocks/errors.py b/packages/liveblocks-python/liveblocks/errors.py
new file mode 100644
index 0000000000..015765f1a0
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/errors.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+import json
+from typing import Any
+
+import httpx
+
+
+class LiveblocksError(Exception):
+ def __init__(self, message: str, status: int, details: str | None = None):
+ super().__init__(message)
+ self.status = status
+ self.details = details
+
+ def __str__(self) -> str:
+ msg = f"{self.args[0]} (status {self.status})"
+ if self.details:
+ msg += f"\n{self.details}"
+ return msg
+
+ @classmethod
+ def from_response(cls, response: httpx.Response) -> LiveblocksError:
+ FALLBACK = "An error happened without an error message"
+ try:
+ response.read()
+ text = response.text
+ except Exception:
+ text = FALLBACK
+
+ obj: dict[str, Any]
+ try:
+ parsed = json.loads(text)
+ if isinstance(parsed, dict):
+ obj = parsed
+ else:
+ obj = {"message": text}
+ except Exception:
+ obj = {"message": text}
+
+ message = str(obj.get("message") or FALLBACK)
+
+ parts: list[str] = []
+ if obj.get("suggestion") is not None:
+ parts.append(f"Suggestion: {obj['suggestion']}")
+ if obj.get("docs") is not None:
+ parts.append(f"See also: {obj['docs']}")
+
+ details = "\n".join(parts) or None
+ return cls(message, response.status_code, details)
+
+
+__all__ = ["LiveblocksError"]
diff --git a/packages/liveblocks-python/liveblocks/models/__init__.py b/packages/liveblocks-python/liveblocks/models/__init__.py
new file mode 100644
index 0000000000..39708add6f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/__init__.py
@@ -0,0 +1,391 @@
+"""Contains all the data models used in inputs/outputs"""
+
+from .active_users_response import ActiveUsersResponse
+from .active_users_response_data_item import ActiveUsersResponseDataItem
+from .active_users_response_data_item_info import ActiveUsersResponseDataItemInfo
+from .add_comment_reaction_request_body import AddCommentReactionRequestBody
+from .add_group_members_request_body import AddGroupMembersRequestBody
+from .add_json_patch_operation import AddJsonPatchOperation
+from .ai_copilot_anthropic import AiCopilotAnthropic
+from .ai_copilot_base import AiCopilotBase
+from .ai_copilot_google import AiCopilotGoogle
+from .ai_copilot_open_ai import AiCopilotOpenAi
+from .ai_copilot_open_ai_compatible import AiCopilotOpenAiCompatible
+from .ai_copilot_provider_settings import AiCopilotProviderSettings
+from .anthropic_model import AnthropicModel
+from .anthropic_provider_options import AnthropicProviderOptions
+from .anthropic_provider_options_anthropic import AnthropicProviderOptionsAnthropic
+from .anthropic_provider_options_anthropic_anthropic_thinking_disabled import (
+ AnthropicProviderOptionsAnthropicAnthropicThinkingDisabled,
+)
+from .anthropic_provider_options_anthropic_anthropic_thinking_enabled import (
+ AnthropicProviderOptionsAnthropicAnthropicThinkingEnabled,
+)
+from .anthropic_provider_options_anthropic_anthropic_web_search import (
+ AnthropicProviderOptionsAnthropicAnthropicWebSearch,
+)
+from .authorization import Authorization
+from .authorize_user_request_body import AuthorizeUserRequestBody
+from .authorize_user_request_body_permissions import AuthorizeUserRequestBodyPermissions
+from .authorize_user_request_body_user_info import AuthorizeUserRequestBodyUserInfo
+from .authorize_user_response import AuthorizeUserResponse
+from .comment import Comment
+from .comment_attachment import CommentAttachment
+from .comment_body import CommentBody
+from .comment_body_content_item import CommentBodyContentItem
+from .comment_metadata import CommentMetadata
+from .comment_reaction import CommentReaction
+from .copy_json_patch_operation import CopyJsonPatchOperation
+from .create_ai_copilot_options_anthropic import CreateAiCopilotOptionsAnthropic
+from .create_ai_copilot_options_base import CreateAiCopilotOptionsBase
+from .create_ai_copilot_options_google import CreateAiCopilotOptionsGoogle
+from .create_ai_copilot_options_open_ai import CreateAiCopilotOptionsOpenAi
+from .create_ai_copilot_options_open_ai_compatible import CreateAiCopilotOptionsOpenAiCompatible
+from .create_comment_request_body import CreateCommentRequestBody
+from .create_file_knowledge_source_response_200 import CreateFileKnowledgeSourceResponse200
+from .create_group_request_body import CreateGroupRequestBody
+from .create_group_request_body_scopes import CreateGroupRequestBodyScopes
+from .create_management_project_request_body import CreateManagementProjectRequestBody
+from .create_management_webhook_request_body import CreateManagementWebhookRequestBody
+from .create_management_webhook_request_body_additional_headers import (
+ CreateManagementWebhookRequestBodyAdditionalHeaders,
+)
+from .create_room_request_body import CreateRoomRequestBody
+from .create_room_request_body_engine import CreateRoomRequestBodyEngine
+from .create_thread_request_body import CreateThreadRequestBody
+from .create_thread_request_body_comment import CreateThreadRequestBodyComment
+from .create_web_knowledge_source_request_body import CreateWebKnowledgeSourceRequestBody
+from .create_web_knowledge_source_request_body_type import CreateWebKnowledgeSourceRequestBodyType
+from .create_web_knowledge_source_response import CreateWebKnowledgeSourceResponse
+from .create_yjs_version_response import CreateYjsVersionResponse
+from .create_yjs_version_response_data import CreateYjsVersionResponseData
+from .delete_management_webhook_headers_request_body import DeleteManagementWebhookHeadersRequestBody
+from .delete_management_webhook_headers_response import DeleteManagementWebhookHeadersResponse
+from .edit_comment_metadata_request_body import EditCommentMetadataRequestBody
+from .edit_comment_metadata_request_body_metadata import EditCommentMetadataRequestBodyMetadata
+from .edit_comment_request_body import EditCommentRequestBody
+from .edit_thread_metadata_request_body import EditThreadMetadataRequestBody
+from .edit_thread_metadata_request_body_metadata import EditThreadMetadataRequestBodyMetadata
+from .error import Error
+from .get_ai_copilots_response import GetAiCopilotsResponse
+from .get_file_knowledge_source_markdown_response import GetFileKnowledgeSourceMarkdownResponse
+from .get_groups_response import GetGroupsResponse
+from .get_inbox_notifications_response import GetInboxNotificationsResponse
+from .get_knowledge_sources_response import GetKnowledgeSourcesResponse
+from .get_management_projects_response import GetManagementProjectsResponse
+from .get_management_webhook_headers_response import GetManagementWebhookHeadersResponse
+from .get_management_webhooks_response import GetManagementWebhooksResponse
+from .get_room_subscription_settings_response import GetRoomSubscriptionSettingsResponse
+from .get_rooms_response import GetRoomsResponse
+from .get_storage_document_format import GetStorageDocumentFormat
+from .get_storage_document_response import GetStorageDocumentResponse
+from .get_thread_subscriptions_response import GetThreadSubscriptionsResponse
+from .get_threads_response import GetThreadsResponse
+from .get_user_groups_response import GetUserGroupsResponse
+from .get_web_knowledge_source_links_response import GetWebKnowledgeSourceLinksResponse
+from .get_yjs_document_response import GetYjsDocumentResponse
+from .get_yjs_document_type import GetYjsDocumentType
+from .get_yjs_versions_response import GetYjsVersionsResponse
+from .google_model import GoogleModel
+from .google_provider_options import GoogleProviderOptions
+from .google_provider_options_google import GoogleProviderOptionsGoogle
+from .google_provider_options_google_thinking_config import GoogleProviderOptionsGoogleThinkingConfig
+from .group import Group
+from .group_member import GroupMember
+from .group_scopes import GroupScopes
+from .identify_user_request_body import IdentifyUserRequestBody
+from .identify_user_request_body_user_info import IdentifyUserRequestBodyUserInfo
+from .identify_user_response import IdentifyUserResponse
+from .inbox_notification_activity import InboxNotificationActivity
+from .inbox_notification_activity_data import InboxNotificationActivityData
+from .inbox_notification_custom_data import InboxNotificationCustomData
+from .inbox_notification_thread_data import InboxNotificationThreadData
+from .initialize_storage_document_body import InitializeStorageDocumentBody
+from .initialize_storage_document_body_data import InitializeStorageDocumentBodyData
+from .initialize_storage_document_response import InitializeStorageDocumentResponse
+from .initialize_storage_document_response_data import InitializeStorageDocumentResponseData
+from .knowledge_source_base import KnowledgeSourceBase
+from .knowledge_source_base_status import KnowledgeSourceBaseStatus
+from .knowledge_source_file_source import KnowledgeSourceFileSource
+from .knowledge_source_file_source_file import KnowledgeSourceFileSourceFile
+from .knowledge_source_web_source import KnowledgeSourceWebSource
+from .knowledge_source_web_source_link import KnowledgeSourceWebSourceLink
+from .knowledge_source_web_source_link_type import KnowledgeSourceWebSourceLinkType
+from .management_project import ManagementProject
+from .management_project_public_key import ManagementProjectPublicKey
+from .management_project_region import ManagementProjectRegion
+from .management_project_roll_project_secret_api_key_response_secret_key_response import (
+ ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse,
+)
+from .management_project_secret_key import ManagementProjectSecretKey
+from .management_project_type import ManagementProjectType
+from .management_webhook import ManagementWebhook
+from .management_webhook_additional_headers import ManagementWebhookAdditionalHeaders
+from .management_webhook_event import ManagementWebhookEvent
+from .management_webhook_headers_delete import ManagementWebhookHeadersDelete
+from .management_webhook_secret import ManagementWebhookSecret
+from .mark_thread_as_resolved_request_body import MarkThreadAsResolvedRequestBody
+from .mark_thread_as_unresolved_request_body import MarkThreadAsUnresolvedRequestBody
+from .move_json_patch_operation import MoveJsonPatchOperation
+from .notification_channel_settings import NotificationChannelSettings
+from .notification_settings import NotificationSettings
+from .open_ai_model import OpenAiModel
+from .open_ai_provider_options import OpenAiProviderOptions
+from .open_ai_provider_options_openai import OpenAiProviderOptionsOpenai
+from .open_ai_provider_options_openai_reasoning_effort import OpenAiProviderOptionsOpenaiReasoningEffort
+from .open_ai_provider_options_openai_web_search import OpenAiProviderOptionsOpenaiWebSearch
+from .recover_management_webhook_failed_messages_request_body import RecoverManagementWebhookFailedMessagesRequestBody
+from .remove_comment_reaction_request_body import RemoveCommentReactionRequestBody
+from .remove_group_members_request_body import RemoveGroupMembersRequestBody
+from .remove_json_patch_operation import RemoveJsonPatchOperation
+from .replace_json_patch_operation import ReplaceJsonPatchOperation
+from .roll_project_public_api_key_request_body import RollProjectPublicApiKeyRequestBody
+from .roll_project_public_api_key_request_body_expiration_in import RollProjectPublicApiKeyRequestBodyExpirationIn
+from .roll_project_public_api_key_response import RollProjectPublicApiKeyResponse
+from .roll_project_secret_api_key_request_body import RollProjectSecretApiKeyRequestBody
+from .roll_project_secret_api_key_request_body_expiration_in import RollProjectSecretApiKeyRequestBodyExpirationIn
+from .room import Room
+from .room_accesses import RoomAccesses
+from .room_accesses_additional_property_item import RoomAccessesAdditionalPropertyItem
+from .room_metadata import RoomMetadata
+from .room_permission_item import RoomPermissionItem
+from .room_subscription_settings import RoomSubscriptionSettings
+from .room_subscription_settings_text_mentions import RoomSubscriptionSettingsTextMentions
+from .room_subscription_settings_threads import RoomSubscriptionSettingsThreads
+from .room_type import RoomType
+from .rotate_management_webhook_secret_response import RotateManagementWebhookSecretResponse
+from .set_presence_request_body import SetPresenceRequestBody
+from .set_presence_request_body_data import SetPresenceRequestBodyData
+from .set_presence_request_body_user_info import SetPresenceRequestBodyUserInfo
+from .subscribe_to_thread_request_body import SubscribeToThreadRequestBody
+from .subscription import Subscription
+from .test_json_patch_operation import TestJsonPatchOperation
+from .test_management_webhook_request_body import TestManagementWebhookRequestBody
+from .test_management_webhook_response import TestManagementWebhookResponse
+from .test_management_webhook_response_message import TestManagementWebhookResponseMessage
+from .thread import Thread
+from .thread_metadata import ThreadMetadata
+from .trigger_inbox_notification_request_body import TriggerInboxNotificationRequestBody
+from .trigger_inbox_notification_request_body_activity_data import TriggerInboxNotificationRequestBodyActivityData
+from .unsubscribe_from_thread_request_body import UnsubscribeFromThreadRequestBody
+from .update_ai_copilot_request_body import UpdateAiCopilotRequestBody
+from .update_ai_copilot_request_body_provider import UpdateAiCopilotRequestBodyProvider
+from .update_management_project_request_body import UpdateManagementProjectRequestBody
+from .update_management_webhook_request_body import UpdateManagementWebhookRequestBody
+from .update_notification_settings_request_body import UpdateNotificationSettingsRequestBody
+from .update_room_id_request_body import UpdateRoomIdRequestBody
+from .update_room_request_body import UpdateRoomRequestBody
+from .update_room_request_body_groups_accesses import UpdateRoomRequestBodyGroupsAccesses
+from .update_room_request_body_groups_accesses_additional_property_type_0_item import (
+ UpdateRoomRequestBodyGroupsAccessesAdditionalPropertyType0Item,
+)
+from .update_room_request_body_metadata import UpdateRoomRequestBodyMetadata
+from .update_room_request_body_users_accesses import UpdateRoomRequestBodyUsersAccesses
+from .update_room_request_body_users_accesses_additional_property_type_0_item import (
+ UpdateRoomRequestBodyUsersAccessesAdditionalPropertyType0Item,
+)
+from .update_room_subscription_settings_request_body import UpdateRoomSubscriptionSettingsRequestBody
+from .update_room_subscription_settings_request_body_text_mentions import (
+ UpdateRoomSubscriptionSettingsRequestBodyTextMentions,
+)
+from .update_room_subscription_settings_request_body_threads import UpdateRoomSubscriptionSettingsRequestBodyThreads
+from .upsert_management_webhook_headers_request_body import UpsertManagementWebhookHeadersRequestBody
+from .upsert_management_webhook_headers_response import UpsertManagementWebhookHeadersResponse
+from .upsert_management_webhook_headers_response_headers import UpsertManagementWebhookHeadersResponseHeaders
+from .upsert_room_request_body import UpsertRoomRequestBody
+from .user_room_subscription_settings import UserRoomSubscriptionSettings
+from .user_room_subscription_settings_text_mentions import UserRoomSubscriptionSettingsTextMentions
+from .user_room_subscription_settings_threads import UserRoomSubscriptionSettingsThreads
+from .user_subscription import UserSubscription
+from .web_knowledge_source_link import WebKnowledgeSourceLink
+from .web_knowledge_source_link_status import WebKnowledgeSourceLinkStatus
+from .yjs_version import YjsVersion
+from .yjs_version_authors_item import YjsVersionAuthorsItem
+
+__all__ = (
+ "ActiveUsersResponse",
+ "ActiveUsersResponseDataItem",
+ "ActiveUsersResponseDataItemInfo",
+ "AddCommentReactionRequestBody",
+ "AddGroupMembersRequestBody",
+ "AddJsonPatchOperation",
+ "AiCopilotAnthropic",
+ "AiCopilotBase",
+ "AiCopilotGoogle",
+ "AiCopilotOpenAi",
+ "AiCopilotOpenAiCompatible",
+ "AiCopilotProviderSettings",
+ "AnthropicModel",
+ "AnthropicProviderOptions",
+ "AnthropicProviderOptionsAnthropic",
+ "AnthropicProviderOptionsAnthropicAnthropicThinkingDisabled",
+ "AnthropicProviderOptionsAnthropicAnthropicThinkingEnabled",
+ "AnthropicProviderOptionsAnthropicAnthropicWebSearch",
+ "Authorization",
+ "AuthorizeUserRequestBody",
+ "AuthorizeUserRequestBodyPermissions",
+ "AuthorizeUserRequestBodyUserInfo",
+ "AuthorizeUserResponse",
+ "Comment",
+ "CommentAttachment",
+ "CommentBody",
+ "CommentBodyContentItem",
+ "CommentMetadata",
+ "CommentReaction",
+ "CopyJsonPatchOperation",
+ "CreateAiCopilotOptionsAnthropic",
+ "CreateAiCopilotOptionsBase",
+ "CreateAiCopilotOptionsGoogle",
+ "CreateAiCopilotOptionsOpenAi",
+ "CreateAiCopilotOptionsOpenAiCompatible",
+ "CreateCommentRequestBody",
+ "CreateFileKnowledgeSourceResponse200",
+ "CreateGroupRequestBody",
+ "CreateGroupRequestBodyScopes",
+ "CreateManagementProjectRequestBody",
+ "CreateManagementWebhookRequestBody",
+ "CreateManagementWebhookRequestBodyAdditionalHeaders",
+ "CreateRoomRequestBody",
+ "CreateRoomRequestBodyEngine",
+ "CreateThreadRequestBody",
+ "CreateThreadRequestBodyComment",
+ "CreateWebKnowledgeSourceRequestBody",
+ "CreateWebKnowledgeSourceRequestBodyType",
+ "CreateWebKnowledgeSourceResponse",
+ "CreateYjsVersionResponse",
+ "CreateYjsVersionResponseData",
+ "DeleteManagementWebhookHeadersRequestBody",
+ "DeleteManagementWebhookHeadersResponse",
+ "EditCommentMetadataRequestBody",
+ "EditCommentMetadataRequestBodyMetadata",
+ "EditCommentRequestBody",
+ "EditThreadMetadataRequestBody",
+ "EditThreadMetadataRequestBodyMetadata",
+ "Error",
+ "GetAiCopilotsResponse",
+ "GetFileKnowledgeSourceMarkdownResponse",
+ "GetGroupsResponse",
+ "GetInboxNotificationsResponse",
+ "GetKnowledgeSourcesResponse",
+ "GetManagementProjectsResponse",
+ "GetManagementWebhookHeadersResponse",
+ "GetManagementWebhooksResponse",
+ "GetRoomsResponse",
+ "GetRoomSubscriptionSettingsResponse",
+ "GetStorageDocumentFormat",
+ "GetStorageDocumentResponse",
+ "GetThreadsResponse",
+ "GetThreadSubscriptionsResponse",
+ "GetUserGroupsResponse",
+ "GetWebKnowledgeSourceLinksResponse",
+ "GetYjsDocumentResponse",
+ "GetYjsDocumentType",
+ "GetYjsVersionsResponse",
+ "GoogleModel",
+ "GoogleProviderOptions",
+ "GoogleProviderOptionsGoogle",
+ "GoogleProviderOptionsGoogleThinkingConfig",
+ "Group",
+ "GroupMember",
+ "GroupScopes",
+ "IdentifyUserRequestBody",
+ "IdentifyUserRequestBodyUserInfo",
+ "IdentifyUserResponse",
+ "InboxNotificationActivity",
+ "InboxNotificationActivityData",
+ "InboxNotificationCustomData",
+ "InboxNotificationThreadData",
+ "InitializeStorageDocumentBody",
+ "InitializeStorageDocumentBodyData",
+ "InitializeStorageDocumentResponse",
+ "InitializeStorageDocumentResponseData",
+ "KnowledgeSourceBase",
+ "KnowledgeSourceBaseStatus",
+ "KnowledgeSourceFileSource",
+ "KnowledgeSourceFileSourceFile",
+ "KnowledgeSourceWebSource",
+ "KnowledgeSourceWebSourceLink",
+ "KnowledgeSourceWebSourceLinkType",
+ "ManagementProject",
+ "ManagementProjectPublicKey",
+ "ManagementProjectRegion",
+ "ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse",
+ "ManagementProjectSecretKey",
+ "ManagementProjectType",
+ "ManagementWebhook",
+ "ManagementWebhookAdditionalHeaders",
+ "ManagementWebhookEvent",
+ "ManagementWebhookHeadersDelete",
+ "ManagementWebhookSecret",
+ "MarkThreadAsResolvedRequestBody",
+ "MarkThreadAsUnresolvedRequestBody",
+ "MoveJsonPatchOperation",
+ "NotificationChannelSettings",
+ "NotificationSettings",
+ "OpenAiModel",
+ "OpenAiProviderOptions",
+ "OpenAiProviderOptionsOpenai",
+ "OpenAiProviderOptionsOpenaiReasoningEffort",
+ "OpenAiProviderOptionsOpenaiWebSearch",
+ "RecoverManagementWebhookFailedMessagesRequestBody",
+ "RemoveCommentReactionRequestBody",
+ "RemoveGroupMembersRequestBody",
+ "RemoveJsonPatchOperation",
+ "ReplaceJsonPatchOperation",
+ "RollProjectPublicApiKeyRequestBody",
+ "RollProjectPublicApiKeyRequestBodyExpirationIn",
+ "RollProjectPublicApiKeyResponse",
+ "RollProjectSecretApiKeyRequestBody",
+ "RollProjectSecretApiKeyRequestBodyExpirationIn",
+ "Room",
+ "RoomAccesses",
+ "RoomAccessesAdditionalPropertyItem",
+ "RoomMetadata",
+ "RoomPermissionItem",
+ "RoomSubscriptionSettings",
+ "RoomSubscriptionSettingsTextMentions",
+ "RoomSubscriptionSettingsThreads",
+ "RoomType",
+ "RotateManagementWebhookSecretResponse",
+ "SetPresenceRequestBody",
+ "SetPresenceRequestBodyData",
+ "SetPresenceRequestBodyUserInfo",
+ "SubscribeToThreadRequestBody",
+ "Subscription",
+ "TestJsonPatchOperation",
+ "TestManagementWebhookRequestBody",
+ "TestManagementWebhookResponse",
+ "TestManagementWebhookResponseMessage",
+ "Thread",
+ "ThreadMetadata",
+ "TriggerInboxNotificationRequestBody",
+ "TriggerInboxNotificationRequestBodyActivityData",
+ "UnsubscribeFromThreadRequestBody",
+ "UpdateAiCopilotRequestBody",
+ "UpdateAiCopilotRequestBodyProvider",
+ "UpdateManagementProjectRequestBody",
+ "UpdateManagementWebhookRequestBody",
+ "UpdateNotificationSettingsRequestBody",
+ "UpdateRoomIdRequestBody",
+ "UpdateRoomRequestBody",
+ "UpdateRoomRequestBodyGroupsAccesses",
+ "UpdateRoomRequestBodyGroupsAccessesAdditionalPropertyType0Item",
+ "UpdateRoomRequestBodyMetadata",
+ "UpdateRoomRequestBodyUsersAccesses",
+ "UpdateRoomRequestBodyUsersAccessesAdditionalPropertyType0Item",
+ "UpdateRoomSubscriptionSettingsRequestBody",
+ "UpdateRoomSubscriptionSettingsRequestBodyTextMentions",
+ "UpdateRoomSubscriptionSettingsRequestBodyThreads",
+ "UpsertManagementWebhookHeadersRequestBody",
+ "UpsertManagementWebhookHeadersResponse",
+ "UpsertManagementWebhookHeadersResponseHeaders",
+ "UpsertRoomRequestBody",
+ "UserRoomSubscriptionSettings",
+ "UserRoomSubscriptionSettingsTextMentions",
+ "UserRoomSubscriptionSettingsThreads",
+ "UserSubscription",
+ "WebKnowledgeSourceLink",
+ "WebKnowledgeSourceLinkStatus",
+ "YjsVersion",
+ "YjsVersionAuthorsItem",
+)
diff --git a/packages/liveblocks-python/liveblocks/models/active_users_response.py b/packages/liveblocks-python/liveblocks/models/active_users_response.py
new file mode 100644
index 0000000000..d6f5e1e33e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/active_users_response.py
@@ -0,0 +1,76 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+if TYPE_CHECKING:
+ from ..models.active_users_response_data_item import ActiveUsersResponseDataItem
+
+
+@_attrs_define
+class ActiveUsersResponse:
+ """
+ Example:
+ {'data': [{'type': 'user', 'connectionId': 16, 'id': 'alice', 'info': {}}, {'type': 'user', 'connectionId': 20,
+ 'id': 'bob', 'info': {}}]}
+
+ Attributes:
+ data (list[ActiveUsersResponseDataItem]):
+ """
+
+ data: list[ActiveUsersResponseDataItem]
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.active_users_response_data_item import ActiveUsersResponseDataItem
+
+ d = dict(src_dict)
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+ data_item = ActiveUsersResponseDataItem.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ active_users_response = cls(
+ data=data,
+ )
+
+ active_users_response.additional_properties = d
+ return active_users_response
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/active_users_response_data_item.py b/packages/liveblocks-python/liveblocks/models/active_users_response_data_item.py
new file mode 100644
index 0000000000..c77e104fc0
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/active_users_response_data_item.py
@@ -0,0 +1,96 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+if TYPE_CHECKING:
+ from ..models.active_users_response_data_item_info import ActiveUsersResponseDataItemInfo
+
+
+@_attrs_define
+class ActiveUsersResponseDataItem:
+ """
+ Attributes:
+ type_ (Literal['user']):
+ id (None | str):
+ info (ActiveUsersResponseDataItemInfo):
+ connection_id (int):
+ """
+
+ type_: Literal["user"]
+ id: None | str
+ info: ActiveUsersResponseDataItemInfo
+ connection_id: int
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ id: None | str
+ id = self.id
+
+ info = self.info.to_dict()
+
+ connection_id = self.connection_id
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "type": type_,
+ "id": id,
+ "info": info,
+ "connectionId": connection_id,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.active_users_response_data_item_info import ActiveUsersResponseDataItemInfo
+
+ d = dict(src_dict)
+ type_ = cast(Literal["user"], d.pop("type"))
+ if type_ != "user":
+ raise ValueError(f"type must match const 'user', got '{type_}'")
+
+ def _parse_id(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ id = _parse_id(d.pop("id"))
+
+ info = ActiveUsersResponseDataItemInfo.from_dict(d.pop("info"))
+
+ connection_id = d.pop("connectionId")
+
+ active_users_response_data_item = cls(
+ type_=type_,
+ id=id,
+ info=info,
+ connection_id=connection_id,
+ )
+
+ active_users_response_data_item.additional_properties = d
+ return active_users_response_data_item
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/active_users_response_data_item_info.py b/packages/liveblocks-python/liveblocks/models/active_users_response_data_item_info.py
new file mode 100644
index 0000000000..5e62f7e926
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/active_users_response_data_item_info.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class ActiveUsersResponseDataItemInfo:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ active_users_response_data_item_info = cls()
+
+ active_users_response_data_item_info.additional_properties = d
+ return active_users_response_data_item_info
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/add_comment_reaction_request_body.py b/packages/liveblocks-python/liveblocks/models/add_comment_reaction_request_body.py
new file mode 100644
index 0000000000..f53f432e2c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/add_comment_reaction_request_body.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class AddCommentReactionRequestBody:
+ """
+ Attributes:
+ user_id (str):
+ emoji (str):
+ created_at (datetime.datetime | Unset):
+ """
+
+ user_id: str
+ emoji: str
+ created_at: datetime.datetime | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ emoji = self.emoji
+
+ created_at: str | Unset = UNSET
+ if not isinstance(self.created_at, Unset):
+ created_at = self.created_at.isoformat()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ "emoji": emoji,
+ }
+ )
+ if created_at is not UNSET:
+ field_dict["createdAt"] = created_at
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ emoji = d.pop("emoji")
+
+ _created_at = d.pop("createdAt", UNSET)
+ created_at: datetime.datetime | Unset
+ if isinstance(_created_at, Unset):
+ created_at = UNSET
+ else:
+ created_at = isoparse(_created_at)
+
+ add_comment_reaction_request_body = cls(
+ user_id=user_id,
+ emoji=emoji,
+ created_at=created_at,
+ )
+
+ return add_comment_reaction_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/add_group_members_request_body.py b/packages/liveblocks-python/liveblocks/models/add_group_members_request_body.py
new file mode 100644
index 0000000000..9a678da509
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/add_group_members_request_body.py
@@ -0,0 +1,62 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class AddGroupMembersRequestBody:
+ """
+ Example:
+ {'memberIds': ['charlie', 'dave']}
+
+ Attributes:
+ member_ids (list[str]):
+ """
+
+ member_ids: list[str]
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ member_ids = self.member_ids
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "memberIds": member_ids,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ member_ids = cast(list[str], d.pop("memberIds"))
+
+ add_group_members_request_body = cls(
+ member_ids=member_ids,
+ )
+
+ add_group_members_request_body.additional_properties = d
+ return add_group_members_request_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/add_json_patch_operation.py b/packages/liveblocks-python/liveblocks/models/add_json_patch_operation.py
new file mode 100644
index 0000000000..ed6becfb1f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/add_json_patch_operation.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class AddJsonPatchOperation:
+ """
+ Attributes:
+ op (Literal['add']):
+ path (str): A JSON Pointer to the target location (RFC 6901). Must start with "/".
+ value (Any):
+ """
+
+ op: Literal["add"]
+ path: str
+ value: Any
+
+ def to_dict(self) -> dict[str, Any]:
+ op = self.op
+
+ path = self.path
+
+ value = self.value
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "op": op,
+ "path": path,
+ "value": value,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ op = cast(Literal["add"], d.pop("op"))
+ if op != "add":
+ raise ValueError(f"op must match const 'add', got '{op}'")
+
+ path = d.pop("path")
+
+ value = d.pop("value")
+
+ add_json_patch_operation = cls(
+ op=op,
+ path=path,
+ value=value,
+ )
+
+ return add_json_patch_operation
diff --git a/packages/liveblocks-python/liveblocks/models/ai_copilot_anthropic.py b/packages/liveblocks-python/liveblocks/models/ai_copilot_anthropic.py
new file mode 100644
index 0000000000..d36ded6bc9
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/ai_copilot_anthropic.py
@@ -0,0 +1,192 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..models.anthropic_model import AnthropicModel
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.anthropic_provider_options import AnthropicProviderOptions
+
+
+@_attrs_define
+class AiCopilotAnthropic:
+ """
+ Example:
+ {'type': 'copilot', 'id': 'cp_abc456', 'name': 'My Anthropic Copilot', 'systemPrompt': 'You are a helpful
+ assistant.', 'alwaysUseKnowledge': True, 'createdAt': '2024-06-01T12:00:00.000Z', 'updatedAt':
+ '2024-06-01T12:00:00.000Z', 'provider': 'anthropic', 'providerModel': 'claude-3-5-sonnet-latest'}
+
+ Attributes:
+ type_ (Literal['copilot']):
+ id (str):
+ name (str):
+ system_prompt (str):
+ always_use_knowledge (bool):
+ created_at (datetime.datetime):
+ updated_at (datetime.datetime):
+ provider (Literal['anthropic']):
+ provider_model (AnthropicModel): Example: claude-3-5-sonnet-latest.
+ description (str | Unset):
+ knowledge_prompt (str | Unset):
+ last_used_at (datetime.datetime | Unset):
+ settings (AiCopilotProviderSettings | Unset): Example: {'maxTokens': 4096, 'temperature': 0.7, 'topP': 0.9}.
+ provider_options (AnthropicProviderOptions | Unset): Example: {'anthropic': {'thinking': {'type': 'enabled',
+ 'budgetTokens': 10000}}}.
+ """
+
+ type_: Literal["copilot"]
+ id: str
+ name: str
+ system_prompt: str
+ always_use_knowledge: bool
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ provider: Literal["anthropic"]
+ provider_model: AnthropicModel
+ description: str | Unset = UNSET
+ knowledge_prompt: str | Unset = UNSET
+ last_used_at: datetime.datetime | Unset = UNSET
+ settings: AiCopilotProviderSettings | Unset = UNSET
+ provider_options: AnthropicProviderOptions | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ id = self.id
+
+ name = self.name
+
+ system_prompt = self.system_prompt
+
+ always_use_knowledge = self.always_use_knowledge
+
+ created_at = self.created_at.isoformat()
+
+ updated_at = self.updated_at.isoformat()
+
+ provider = self.provider
+
+ provider_model = self.provider_model.value
+
+ description = self.description
+
+ knowledge_prompt = self.knowledge_prompt
+
+ last_used_at: str | Unset = UNSET
+ if not isinstance(self.last_used_at, Unset):
+ last_used_at = self.last_used_at.isoformat()
+
+ settings: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.settings, Unset):
+ settings = self.settings.to_dict()
+
+ provider_options: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.provider_options, Unset):
+ provider_options = self.provider_options.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "type": type_,
+ "id": id,
+ "name": name,
+ "systemPrompt": system_prompt,
+ "alwaysUseKnowledge": always_use_knowledge,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ "provider": provider,
+ "providerModel": provider_model,
+ }
+ )
+ if description is not UNSET:
+ field_dict["description"] = description
+ if knowledge_prompt is not UNSET:
+ field_dict["knowledgePrompt"] = knowledge_prompt
+ if last_used_at is not UNSET:
+ field_dict["lastUsedAt"] = last_used_at
+ if settings is not UNSET:
+ field_dict["settings"] = settings
+ if provider_options is not UNSET:
+ field_dict["providerOptions"] = provider_options
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.anthropic_provider_options import AnthropicProviderOptions
+
+ d = dict(src_dict)
+ type_ = cast(Literal["copilot"], d.pop("type"))
+ if type_ != "copilot":
+ raise ValueError(f"type must match const 'copilot', got '{type_}'")
+
+ id = d.pop("id")
+
+ name = d.pop("name")
+
+ system_prompt = d.pop("systemPrompt")
+
+ always_use_knowledge = d.pop("alwaysUseKnowledge")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ provider = cast(Literal["anthropic"], d.pop("provider"))
+ if provider != "anthropic":
+ raise ValueError(f"provider must match const 'anthropic', got '{provider}'")
+
+ provider_model = AnthropicModel(d.pop("providerModel"))
+
+ description = d.pop("description", UNSET)
+
+ knowledge_prompt = d.pop("knowledgePrompt", UNSET)
+
+ _last_used_at = d.pop("lastUsedAt", UNSET)
+ last_used_at: datetime.datetime | Unset
+ if isinstance(_last_used_at, Unset):
+ last_used_at = UNSET
+ else:
+ last_used_at = isoparse(_last_used_at)
+
+ _settings = d.pop("settings", UNSET)
+ settings: AiCopilotProviderSettings | Unset
+ if isinstance(_settings, Unset):
+ settings = UNSET
+ else:
+ settings = AiCopilotProviderSettings.from_dict(_settings)
+
+ _provider_options = d.pop("providerOptions", UNSET)
+ provider_options: AnthropicProviderOptions | Unset
+ if isinstance(_provider_options, Unset):
+ provider_options = UNSET
+ else:
+ provider_options = AnthropicProviderOptions.from_dict(_provider_options)
+
+ ai_copilot_anthropic = cls(
+ type_=type_,
+ id=id,
+ name=name,
+ system_prompt=system_prompt,
+ always_use_knowledge=always_use_knowledge,
+ created_at=created_at,
+ updated_at=updated_at,
+ provider=provider,
+ provider_model=provider_model,
+ description=description,
+ knowledge_prompt=knowledge_prompt,
+ last_used_at=last_used_at,
+ settings=settings,
+ provider_options=provider_options,
+ )
+
+ return ai_copilot_anthropic
diff --git a/packages/liveblocks-python/liveblocks/models/ai_copilot_base.py b/packages/liveblocks-python/liveblocks/models/ai_copilot_base.py
new file mode 100644
index 0000000000..fe1d092dbb
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/ai_copilot_base.py
@@ -0,0 +1,154 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+
+
+@_attrs_define
+class AiCopilotBase:
+ """
+ Example:
+ {'type': 'copilot', 'id': 'cp_abc123', 'name': 'My Copilot', 'systemPrompt': 'You are a helpful assistant.',
+ 'alwaysUseKnowledge': True, 'createdAt': '2024-06-01T12:00:00.000Z', 'updatedAt': '2024-06-01T12:00:00.000Z',
+ 'settings': {'maxTokens': 4096, 'temperature': 0.7}}
+
+ Attributes:
+ type_ (Literal['copilot']):
+ id (str):
+ name (str):
+ system_prompt (str):
+ always_use_knowledge (bool):
+ created_at (datetime.datetime):
+ updated_at (datetime.datetime):
+ description (str | Unset):
+ knowledge_prompt (str | Unset):
+ last_used_at (datetime.datetime | Unset):
+ settings (AiCopilotProviderSettings | Unset): Example: {'maxTokens': 4096, 'temperature': 0.7, 'topP': 0.9}.
+ """
+
+ type_: Literal["copilot"]
+ id: str
+ name: str
+ system_prompt: str
+ always_use_knowledge: bool
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ description: str | Unset = UNSET
+ knowledge_prompt: str | Unset = UNSET
+ last_used_at: datetime.datetime | Unset = UNSET
+ settings: AiCopilotProviderSettings | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ id = self.id
+
+ name = self.name
+
+ system_prompt = self.system_prompt
+
+ always_use_knowledge = self.always_use_knowledge
+
+ created_at = self.created_at.isoformat()
+
+ updated_at = self.updated_at.isoformat()
+
+ description = self.description
+
+ knowledge_prompt = self.knowledge_prompt
+
+ last_used_at: str | Unset = UNSET
+ if not isinstance(self.last_used_at, Unset):
+ last_used_at = self.last_used_at.isoformat()
+
+ settings: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.settings, Unset):
+ settings = self.settings.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "type": type_,
+ "id": id,
+ "name": name,
+ "systemPrompt": system_prompt,
+ "alwaysUseKnowledge": always_use_knowledge,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ }
+ )
+ if description is not UNSET:
+ field_dict["description"] = description
+ if knowledge_prompt is not UNSET:
+ field_dict["knowledgePrompt"] = knowledge_prompt
+ if last_used_at is not UNSET:
+ field_dict["lastUsedAt"] = last_used_at
+ if settings is not UNSET:
+ field_dict["settings"] = settings
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+
+ d = dict(src_dict)
+ type_ = cast(Literal["copilot"], d.pop("type"))
+ if type_ != "copilot":
+ raise ValueError(f"type must match const 'copilot', got '{type_}'")
+
+ id = d.pop("id")
+
+ name = d.pop("name")
+
+ system_prompt = d.pop("systemPrompt")
+
+ always_use_knowledge = d.pop("alwaysUseKnowledge")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ description = d.pop("description", UNSET)
+
+ knowledge_prompt = d.pop("knowledgePrompt", UNSET)
+
+ _last_used_at = d.pop("lastUsedAt", UNSET)
+ last_used_at: datetime.datetime | Unset
+ if isinstance(_last_used_at, Unset):
+ last_used_at = UNSET
+ else:
+ last_used_at = isoparse(_last_used_at)
+
+ _settings = d.pop("settings", UNSET)
+ settings: AiCopilotProviderSettings | Unset
+ if isinstance(_settings, Unset):
+ settings = UNSET
+ else:
+ settings = AiCopilotProviderSettings.from_dict(_settings)
+
+ ai_copilot_base = cls(
+ type_=type_,
+ id=id,
+ name=name,
+ system_prompt=system_prompt,
+ always_use_knowledge=always_use_knowledge,
+ created_at=created_at,
+ updated_at=updated_at,
+ description=description,
+ knowledge_prompt=knowledge_prompt,
+ last_used_at=last_used_at,
+ settings=settings,
+ )
+
+ return ai_copilot_base
diff --git a/packages/liveblocks-python/liveblocks/models/ai_copilot_google.py b/packages/liveblocks-python/liveblocks/models/ai_copilot_google.py
new file mode 100644
index 0000000000..143ed4376b
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/ai_copilot_google.py
@@ -0,0 +1,192 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..models.google_model import GoogleModel
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.google_provider_options import GoogleProviderOptions
+
+
+@_attrs_define
+class AiCopilotGoogle:
+ """
+ Example:
+ {'type': 'copilot', 'id': 'cp_abc789', 'name': 'My Google Copilot', 'systemPrompt': 'You are a helpful
+ assistant.', 'alwaysUseKnowledge': False, 'createdAt': '2024-06-01T12:00:00.000Z', 'updatedAt':
+ '2024-06-01T12:00:00.000Z', 'provider': 'google', 'providerModel': 'gemini-2.5-flash'}
+
+ Attributes:
+ type_ (Literal['copilot']):
+ id (str):
+ name (str):
+ system_prompt (str):
+ always_use_knowledge (bool):
+ created_at (datetime.datetime):
+ updated_at (datetime.datetime):
+ provider (Literal['google']):
+ provider_model (GoogleModel): Example: gemini-2.5-flash.
+ description (str | Unset):
+ knowledge_prompt (str | Unset):
+ last_used_at (datetime.datetime | Unset):
+ settings (AiCopilotProviderSettings | Unset): Example: {'maxTokens': 4096, 'temperature': 0.7, 'topP': 0.9}.
+ provider_options (GoogleProviderOptions | Unset): Example: {'google': {'thinkingConfig': {'thinkingBudget':
+ 10000}}}.
+ """
+
+ type_: Literal["copilot"]
+ id: str
+ name: str
+ system_prompt: str
+ always_use_knowledge: bool
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ provider: Literal["google"]
+ provider_model: GoogleModel
+ description: str | Unset = UNSET
+ knowledge_prompt: str | Unset = UNSET
+ last_used_at: datetime.datetime | Unset = UNSET
+ settings: AiCopilotProviderSettings | Unset = UNSET
+ provider_options: GoogleProviderOptions | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ id = self.id
+
+ name = self.name
+
+ system_prompt = self.system_prompt
+
+ always_use_knowledge = self.always_use_knowledge
+
+ created_at = self.created_at.isoformat()
+
+ updated_at = self.updated_at.isoformat()
+
+ provider = self.provider
+
+ provider_model = self.provider_model.value
+
+ description = self.description
+
+ knowledge_prompt = self.knowledge_prompt
+
+ last_used_at: str | Unset = UNSET
+ if not isinstance(self.last_used_at, Unset):
+ last_used_at = self.last_used_at.isoformat()
+
+ settings: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.settings, Unset):
+ settings = self.settings.to_dict()
+
+ provider_options: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.provider_options, Unset):
+ provider_options = self.provider_options.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "type": type_,
+ "id": id,
+ "name": name,
+ "systemPrompt": system_prompt,
+ "alwaysUseKnowledge": always_use_knowledge,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ "provider": provider,
+ "providerModel": provider_model,
+ }
+ )
+ if description is not UNSET:
+ field_dict["description"] = description
+ if knowledge_prompt is not UNSET:
+ field_dict["knowledgePrompt"] = knowledge_prompt
+ if last_used_at is not UNSET:
+ field_dict["lastUsedAt"] = last_used_at
+ if settings is not UNSET:
+ field_dict["settings"] = settings
+ if provider_options is not UNSET:
+ field_dict["providerOptions"] = provider_options
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.google_provider_options import GoogleProviderOptions
+
+ d = dict(src_dict)
+ type_ = cast(Literal["copilot"], d.pop("type"))
+ if type_ != "copilot":
+ raise ValueError(f"type must match const 'copilot', got '{type_}'")
+
+ id = d.pop("id")
+
+ name = d.pop("name")
+
+ system_prompt = d.pop("systemPrompt")
+
+ always_use_knowledge = d.pop("alwaysUseKnowledge")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ provider = cast(Literal["google"], d.pop("provider"))
+ if provider != "google":
+ raise ValueError(f"provider must match const 'google', got '{provider}'")
+
+ provider_model = GoogleModel(d.pop("providerModel"))
+
+ description = d.pop("description", UNSET)
+
+ knowledge_prompt = d.pop("knowledgePrompt", UNSET)
+
+ _last_used_at = d.pop("lastUsedAt", UNSET)
+ last_used_at: datetime.datetime | Unset
+ if isinstance(_last_used_at, Unset):
+ last_used_at = UNSET
+ else:
+ last_used_at = isoparse(_last_used_at)
+
+ _settings = d.pop("settings", UNSET)
+ settings: AiCopilotProviderSettings | Unset
+ if isinstance(_settings, Unset):
+ settings = UNSET
+ else:
+ settings = AiCopilotProviderSettings.from_dict(_settings)
+
+ _provider_options = d.pop("providerOptions", UNSET)
+ provider_options: GoogleProviderOptions | Unset
+ if isinstance(_provider_options, Unset):
+ provider_options = UNSET
+ else:
+ provider_options = GoogleProviderOptions.from_dict(_provider_options)
+
+ ai_copilot_google = cls(
+ type_=type_,
+ id=id,
+ name=name,
+ system_prompt=system_prompt,
+ always_use_knowledge=always_use_knowledge,
+ created_at=created_at,
+ updated_at=updated_at,
+ provider=provider,
+ provider_model=provider_model,
+ description=description,
+ knowledge_prompt=knowledge_prompt,
+ last_used_at=last_used_at,
+ settings=settings,
+ provider_options=provider_options,
+ )
+
+ return ai_copilot_google
diff --git a/packages/liveblocks-python/liveblocks/models/ai_copilot_open_ai.py b/packages/liveblocks-python/liveblocks/models/ai_copilot_open_ai.py
new file mode 100644
index 0000000000..e99509bb5f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/ai_copilot_open_ai.py
@@ -0,0 +1,173 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..models.open_ai_model import OpenAiModel
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+
+
+@_attrs_define
+class AiCopilotOpenAi:
+ """
+ Example:
+ {'type': 'copilot', 'id': 'cp_abc123', 'name': 'My Copilot', 'systemPrompt': 'You are a helpful assistant.',
+ 'alwaysUseKnowledge': True, 'createdAt': '2024-06-01T12:00:00.000Z', 'updatedAt': '2024-06-01T12:00:00.000Z',
+ 'provider': 'openai', 'providerModel': 'gpt-4o', 'settings': {'maxTokens': 4096, 'temperature': 0.7}}
+
+ Attributes:
+ type_ (Literal['copilot']):
+ id (str):
+ name (str):
+ system_prompt (str):
+ always_use_knowledge (bool):
+ created_at (datetime.datetime):
+ updated_at (datetime.datetime):
+ provider (Literal['openai']):
+ provider_model (OpenAiModel): Example: gpt-4o.
+ description (str | Unset):
+ knowledge_prompt (str | Unset):
+ last_used_at (datetime.datetime | Unset):
+ settings (AiCopilotProviderSettings | Unset): Example: {'maxTokens': 4096, 'temperature': 0.7, 'topP': 0.9}.
+ """
+
+ type_: Literal["copilot"]
+ id: str
+ name: str
+ system_prompt: str
+ always_use_knowledge: bool
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ provider: Literal["openai"]
+ provider_model: OpenAiModel
+ description: str | Unset = UNSET
+ knowledge_prompt: str | Unset = UNSET
+ last_used_at: datetime.datetime | Unset = UNSET
+ settings: AiCopilotProviderSettings | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ id = self.id
+
+ name = self.name
+
+ system_prompt = self.system_prompt
+
+ always_use_knowledge = self.always_use_knowledge
+
+ created_at = self.created_at.isoformat()
+
+ updated_at = self.updated_at.isoformat()
+
+ provider = self.provider
+
+ provider_model = self.provider_model.value
+
+ description = self.description
+
+ knowledge_prompt = self.knowledge_prompt
+
+ last_used_at: str | Unset = UNSET
+ if not isinstance(self.last_used_at, Unset):
+ last_used_at = self.last_used_at.isoformat()
+
+ settings: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.settings, Unset):
+ settings = self.settings.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "type": type_,
+ "id": id,
+ "name": name,
+ "systemPrompt": system_prompt,
+ "alwaysUseKnowledge": always_use_knowledge,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ "provider": provider,
+ "providerModel": provider_model,
+ }
+ )
+ if description is not UNSET:
+ field_dict["description"] = description
+ if knowledge_prompt is not UNSET:
+ field_dict["knowledgePrompt"] = knowledge_prompt
+ if last_used_at is not UNSET:
+ field_dict["lastUsedAt"] = last_used_at
+ if settings is not UNSET:
+ field_dict["settings"] = settings
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+
+ d = dict(src_dict)
+ type_ = cast(Literal["copilot"], d.pop("type"))
+ if type_ != "copilot":
+ raise ValueError(f"type must match const 'copilot', got '{type_}'")
+
+ id = d.pop("id")
+
+ name = d.pop("name")
+
+ system_prompt = d.pop("systemPrompt")
+
+ always_use_knowledge = d.pop("alwaysUseKnowledge")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ provider = cast(Literal["openai"], d.pop("provider"))
+ if provider != "openai":
+ raise ValueError(f"provider must match const 'openai', got '{provider}'")
+
+ provider_model = OpenAiModel(d.pop("providerModel"))
+
+ description = d.pop("description", UNSET)
+
+ knowledge_prompt = d.pop("knowledgePrompt", UNSET)
+
+ _last_used_at = d.pop("lastUsedAt", UNSET)
+ last_used_at: datetime.datetime | Unset
+ if isinstance(_last_used_at, Unset):
+ last_used_at = UNSET
+ else:
+ last_used_at = isoparse(_last_used_at)
+
+ _settings = d.pop("settings", UNSET)
+ settings: AiCopilotProviderSettings | Unset
+ if isinstance(_settings, Unset):
+ settings = UNSET
+ else:
+ settings = AiCopilotProviderSettings.from_dict(_settings)
+
+ ai_copilot_open_ai = cls(
+ type_=type_,
+ id=id,
+ name=name,
+ system_prompt=system_prompt,
+ always_use_knowledge=always_use_knowledge,
+ created_at=created_at,
+ updated_at=updated_at,
+ provider=provider,
+ provider_model=provider_model,
+ description=description,
+ knowledge_prompt=knowledge_prompt,
+ last_used_at=last_used_at,
+ settings=settings,
+ )
+
+ return ai_copilot_open_ai
diff --git a/packages/liveblocks-python/liveblocks/models/ai_copilot_open_ai_compatible.py b/packages/liveblocks-python/liveblocks/models/ai_copilot_open_ai_compatible.py
new file mode 100644
index 0000000000..5905082081
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/ai_copilot_open_ai_compatible.py
@@ -0,0 +1,189 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+
+
+@_attrs_define
+class AiCopilotOpenAiCompatible:
+ """
+ Example:
+ {'type': 'copilot', 'id': 'cp_compat1', 'name': 'My Compatible Copilot', 'systemPrompt': 'You are a helpful
+ assistant.', 'alwaysUseKnowledge': False, 'createdAt': '2024-06-01T12:00:00.000Z', 'updatedAt':
+ '2024-06-01T12:00:00.000Z', 'provider': 'openai-compatible', 'providerModel': 'my-custom-model',
+ 'compatibleProviderName': 'my-provider', 'providerBaseUrl': 'https://api.my-provider.com/v1'}
+
+ Attributes:
+ type_ (Literal['copilot']):
+ id (str):
+ name (str):
+ system_prompt (str):
+ always_use_knowledge (bool):
+ created_at (datetime.datetime):
+ updated_at (datetime.datetime):
+ provider (Literal['openai-compatible']):
+ provider_model (str):
+ compatible_provider_name (str):
+ provider_base_url (str):
+ description (str | Unset):
+ knowledge_prompt (str | Unset):
+ last_used_at (datetime.datetime | Unset):
+ settings (AiCopilotProviderSettings | Unset): Example: {'maxTokens': 4096, 'temperature': 0.7, 'topP': 0.9}.
+ """
+
+ type_: Literal["copilot"]
+ id: str
+ name: str
+ system_prompt: str
+ always_use_knowledge: bool
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ provider: Literal["openai-compatible"]
+ provider_model: str
+ compatible_provider_name: str
+ provider_base_url: str
+ description: str | Unset = UNSET
+ knowledge_prompt: str | Unset = UNSET
+ last_used_at: datetime.datetime | Unset = UNSET
+ settings: AiCopilotProviderSettings | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ id = self.id
+
+ name = self.name
+
+ system_prompt = self.system_prompt
+
+ always_use_knowledge = self.always_use_knowledge
+
+ created_at = self.created_at.isoformat()
+
+ updated_at = self.updated_at.isoformat()
+
+ provider = self.provider
+
+ provider_model = self.provider_model
+
+ compatible_provider_name = self.compatible_provider_name
+
+ provider_base_url = self.provider_base_url
+
+ description = self.description
+
+ knowledge_prompt = self.knowledge_prompt
+
+ last_used_at: str | Unset = UNSET
+ if not isinstance(self.last_used_at, Unset):
+ last_used_at = self.last_used_at.isoformat()
+
+ settings: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.settings, Unset):
+ settings = self.settings.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "type": type_,
+ "id": id,
+ "name": name,
+ "systemPrompt": system_prompt,
+ "alwaysUseKnowledge": always_use_knowledge,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ "provider": provider,
+ "providerModel": provider_model,
+ "compatibleProviderName": compatible_provider_name,
+ "providerBaseUrl": provider_base_url,
+ }
+ )
+ if description is not UNSET:
+ field_dict["description"] = description
+ if knowledge_prompt is not UNSET:
+ field_dict["knowledgePrompt"] = knowledge_prompt
+ if last_used_at is not UNSET:
+ field_dict["lastUsedAt"] = last_used_at
+ if settings is not UNSET:
+ field_dict["settings"] = settings
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+
+ d = dict(src_dict)
+ type_ = cast(Literal["copilot"], d.pop("type"))
+ if type_ != "copilot":
+ raise ValueError(f"type must match const 'copilot', got '{type_}'")
+
+ id = d.pop("id")
+
+ name = d.pop("name")
+
+ system_prompt = d.pop("systemPrompt")
+
+ always_use_knowledge = d.pop("alwaysUseKnowledge")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ provider = cast(Literal["openai-compatible"], d.pop("provider"))
+ if provider != "openai-compatible":
+ raise ValueError(f"provider must match const 'openai-compatible', got '{provider}'")
+
+ provider_model = d.pop("providerModel")
+
+ compatible_provider_name = d.pop("compatibleProviderName")
+
+ provider_base_url = d.pop("providerBaseUrl")
+
+ description = d.pop("description", UNSET)
+
+ knowledge_prompt = d.pop("knowledgePrompt", UNSET)
+
+ _last_used_at = d.pop("lastUsedAt", UNSET)
+ last_used_at: datetime.datetime | Unset
+ if isinstance(_last_used_at, Unset):
+ last_used_at = UNSET
+ else:
+ last_used_at = isoparse(_last_used_at)
+
+ _settings = d.pop("settings", UNSET)
+ settings: AiCopilotProviderSettings | Unset
+ if isinstance(_settings, Unset):
+ settings = UNSET
+ else:
+ settings = AiCopilotProviderSettings.from_dict(_settings)
+
+ ai_copilot_open_ai_compatible = cls(
+ type_=type_,
+ id=id,
+ name=name,
+ system_prompt=system_prompt,
+ always_use_knowledge=always_use_knowledge,
+ created_at=created_at,
+ updated_at=updated_at,
+ provider=provider,
+ provider_model=provider_model,
+ compatible_provider_name=compatible_provider_name,
+ provider_base_url=provider_base_url,
+ description=description,
+ knowledge_prompt=knowledge_prompt,
+ last_used_at=last_used_at,
+ settings=settings,
+ )
+
+ return ai_copilot_open_ai_compatible
diff --git a/packages/liveblocks-python/liveblocks/models/ai_copilot_provider_settings.py b/packages/liveblocks-python/liveblocks/models/ai_copilot_provider_settings.py
new file mode 100644
index 0000000000..6b1a8cb02a
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/ai_copilot_provider_settings.py
@@ -0,0 +1,117 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class AiCopilotProviderSettings:
+ """
+ Example:
+ {'maxTokens': 4096, 'temperature': 0.7, 'topP': 0.9}
+
+ Attributes:
+ max_tokens (int | Unset):
+ temperature (float | Unset):
+ top_p (float | Unset):
+ top_k (float | Unset):
+ frequency_penalty (float | Unset):
+ presence_penalty (float | Unset):
+ stop_sequences (list[str] | Unset):
+ seed (int | Unset):
+ max_retries (int | Unset):
+ """
+
+ max_tokens: int | Unset = UNSET
+ temperature: float | Unset = UNSET
+ top_p: float | Unset = UNSET
+ top_k: float | Unset = UNSET
+ frequency_penalty: float | Unset = UNSET
+ presence_penalty: float | Unset = UNSET
+ stop_sequences: list[str] | Unset = UNSET
+ seed: int | Unset = UNSET
+ max_retries: int | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ max_tokens = self.max_tokens
+
+ temperature = self.temperature
+
+ top_p = self.top_p
+
+ top_k = self.top_k
+
+ frequency_penalty = self.frequency_penalty
+
+ presence_penalty = self.presence_penalty
+
+ stop_sequences: list[str] | Unset = UNSET
+ if not isinstance(self.stop_sequences, Unset):
+ stop_sequences = self.stop_sequences
+
+ seed = self.seed
+
+ max_retries = self.max_retries
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update({})
+ if max_tokens is not UNSET:
+ field_dict["maxTokens"] = max_tokens
+ if temperature is not UNSET:
+ field_dict["temperature"] = temperature
+ if top_p is not UNSET:
+ field_dict["topP"] = top_p
+ if top_k is not UNSET:
+ field_dict["topK"] = top_k
+ if frequency_penalty is not UNSET:
+ field_dict["frequencyPenalty"] = frequency_penalty
+ if presence_penalty is not UNSET:
+ field_dict["presencePenalty"] = presence_penalty
+ if stop_sequences is not UNSET:
+ field_dict["stopSequences"] = stop_sequences
+ if seed is not UNSET:
+ field_dict["seed"] = seed
+ if max_retries is not UNSET:
+ field_dict["maxRetries"] = max_retries
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ max_tokens = d.pop("maxTokens", UNSET)
+
+ temperature = d.pop("temperature", UNSET)
+
+ top_p = d.pop("topP", UNSET)
+
+ top_k = d.pop("topK", UNSET)
+
+ frequency_penalty = d.pop("frequencyPenalty", UNSET)
+
+ presence_penalty = d.pop("presencePenalty", UNSET)
+
+ stop_sequences = cast(list[str], d.pop("stopSequences", UNSET))
+
+ seed = d.pop("seed", UNSET)
+
+ max_retries = d.pop("maxRetries", UNSET)
+
+ ai_copilot_provider_settings = cls(
+ max_tokens=max_tokens,
+ temperature=temperature,
+ top_p=top_p,
+ top_k=top_k,
+ frequency_penalty=frequency_penalty,
+ presence_penalty=presence_penalty,
+ stop_sequences=stop_sequences,
+ seed=seed,
+ max_retries=max_retries,
+ )
+
+ return ai_copilot_provider_settings
diff --git a/packages/liveblocks-python/liveblocks/models/anthropic_model.py b/packages/liveblocks-python/liveblocks/models/anthropic_model.py
new file mode 100644
index 0000000000..2353e913ef
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/anthropic_model.py
@@ -0,0 +1,13 @@
+from enum import StrEnum
+
+
+class AnthropicModel(StrEnum):
+ CLAUDE_3_5_HAIKU_LATEST = "claude-3-5-haiku-latest"
+ CLAUDE_3_5_SONNET_LATEST = "claude-3-5-sonnet-latest"
+ CLAUDE_3_7_SONNET_20250219 = "claude-3-7-sonnet-20250219"
+ CLAUDE_3_OPUS_LATEST = "claude-3-opus-latest"
+ CLAUDE_4_OPUS_20250514 = "claude-4-opus-20250514"
+ CLAUDE_4_SONNET_20250514 = "claude-4-sonnet-20250514"
+ CLAUDE_HAIKU_4_5_20251001 = "claude-haiku-4-5-20251001"
+ CLAUDE_OPUS_4_1_20250805 = "claude-opus-4-1-20250805"
+ CLAUDE_SONNET_4_5_20250929 = "claude-sonnet-4-5-20250929"
diff --git a/packages/liveblocks-python/liveblocks/models/anthropic_provider_options.py b/packages/liveblocks-python/liveblocks/models/anthropic_provider_options.py
new file mode 100644
index 0000000000..b0336b95c3
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/anthropic_provider_options.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.anthropic_provider_options_anthropic import AnthropicProviderOptionsAnthropic
+
+
+@_attrs_define
+class AnthropicProviderOptions:
+ """
+ Example:
+ {'anthropic': {'thinking': {'type': 'enabled', 'budgetTokens': 10000}}}
+
+ Attributes:
+ anthropic (AnthropicProviderOptionsAnthropic):
+ """
+
+ anthropic: AnthropicProviderOptionsAnthropic
+
+ def to_dict(self) -> dict[str, Any]:
+ anthropic = self.anthropic.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "anthropic": anthropic,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.anthropic_provider_options_anthropic import AnthropicProviderOptionsAnthropic
+
+ d = dict(src_dict)
+ anthropic = AnthropicProviderOptionsAnthropic.from_dict(d.pop("anthropic"))
+
+ anthropic_provider_options = cls(
+ anthropic=anthropic,
+ )
+
+ return anthropic_provider_options
diff --git a/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic.py b/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic.py
new file mode 100644
index 0000000000..83393d0f1f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic.py
@@ -0,0 +1,120 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.anthropic_provider_options_anthropic_anthropic_thinking_disabled import (
+ AnthropicProviderOptionsAnthropicAnthropicThinkingDisabled,
+ )
+ from ..models.anthropic_provider_options_anthropic_anthropic_thinking_enabled import (
+ AnthropicProviderOptionsAnthropicAnthropicThinkingEnabled,
+ )
+ from ..models.anthropic_provider_options_anthropic_anthropic_web_search import (
+ AnthropicProviderOptionsAnthropicAnthropicWebSearch,
+ )
+
+
+@_attrs_define
+class AnthropicProviderOptionsAnthropic:
+ """
+ Attributes:
+ thinking (AnthropicProviderOptionsAnthropicAnthropicThinkingDisabled |
+ AnthropicProviderOptionsAnthropicAnthropicThinkingEnabled | Unset):
+ web_search (AnthropicProviderOptionsAnthropicAnthropicWebSearch | Unset):
+ """
+
+ thinking: (
+ AnthropicProviderOptionsAnthropicAnthropicThinkingDisabled
+ | AnthropicProviderOptionsAnthropicAnthropicThinkingEnabled
+ | Unset
+ ) = UNSET
+ web_search: AnthropicProviderOptionsAnthropicAnthropicWebSearch | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ from ..models.anthropic_provider_options_anthropic_anthropic_thinking_enabled import (
+ AnthropicProviderOptionsAnthropicAnthropicThinkingEnabled,
+ )
+
+ thinking: dict[str, Any] | Unset
+ if isinstance(self.thinking, Unset):
+ thinking = UNSET
+ elif isinstance(self.thinking, AnthropicProviderOptionsAnthropicAnthropicThinkingEnabled):
+ thinking = self.thinking.to_dict()
+ else:
+ thinking = self.thinking.to_dict()
+
+ web_search: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.web_search, Unset):
+ web_search = self.web_search.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update({})
+ if thinking is not UNSET:
+ field_dict["thinking"] = thinking
+ if web_search is not UNSET:
+ field_dict["webSearch"] = web_search
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.anthropic_provider_options_anthropic_anthropic_thinking_disabled import (
+ AnthropicProviderOptionsAnthropicAnthropicThinkingDisabled,
+ )
+ from ..models.anthropic_provider_options_anthropic_anthropic_thinking_enabled import (
+ AnthropicProviderOptionsAnthropicAnthropicThinkingEnabled,
+ )
+ from ..models.anthropic_provider_options_anthropic_anthropic_web_search import (
+ AnthropicProviderOptionsAnthropicAnthropicWebSearch,
+ )
+
+ d = dict(src_dict)
+
+ def _parse_thinking(
+ data: object,
+ ) -> (
+ AnthropicProviderOptionsAnthropicAnthropicThinkingDisabled
+ | AnthropicProviderOptionsAnthropicAnthropicThinkingEnabled
+ | Unset
+ ):
+ if isinstance(data, Unset):
+ return data
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ thinking_anthropic_thinking_enabled = (
+ AnthropicProviderOptionsAnthropicAnthropicThinkingEnabled.from_dict(data)
+ )
+
+ return thinking_anthropic_thinking_enabled
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ if not isinstance(data, dict):
+ raise TypeError()
+ thinking_anthropic_thinking_disabled = AnthropicProviderOptionsAnthropicAnthropicThinkingDisabled.from_dict(
+ data
+ )
+
+ return thinking_anthropic_thinking_disabled
+
+ thinking = _parse_thinking(d.pop("thinking", UNSET))
+
+ _web_search = d.pop("webSearch", UNSET)
+ web_search: AnthropicProviderOptionsAnthropicAnthropicWebSearch | Unset
+ if isinstance(_web_search, Unset):
+ web_search = UNSET
+ else:
+ web_search = AnthropicProviderOptionsAnthropicAnthropicWebSearch.from_dict(_web_search)
+
+ anthropic_provider_options_anthropic = cls(
+ thinking=thinking,
+ web_search=web_search,
+ )
+
+ return anthropic_provider_options_anthropic
diff --git a/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic_anthropic_thinking_disabled.py b/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic_anthropic_thinking_disabled.py
new file mode 100644
index 0000000000..be8efa03a6
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic_anthropic_thinking_disabled.py
@@ -0,0 +1,42 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class AnthropicProviderOptionsAnthropicAnthropicThinkingDisabled:
+ """
+ Attributes:
+ type_ (Literal['disabled']):
+ """
+
+ type_: Literal["disabled"]
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "type": type_,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ type_ = cast(Literal["disabled"], d.pop("type"))
+ if type_ != "disabled":
+ raise ValueError(f"type must match const 'disabled', got '{type_}'")
+
+ anthropic_provider_options_anthropic_anthropic_thinking_disabled = cls(
+ type_=type_,
+ )
+
+ return anthropic_provider_options_anthropic_anthropic_thinking_disabled
diff --git a/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic_anthropic_thinking_enabled.py b/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic_anthropic_thinking_enabled.py
new file mode 100644
index 0000000000..ffd9af4241
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic_anthropic_thinking_enabled.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class AnthropicProviderOptionsAnthropicAnthropicThinkingEnabled:
+ """
+ Attributes:
+ type_ (Literal['enabled']):
+ budget_tokens (int):
+ """
+
+ type_: Literal["enabled"]
+ budget_tokens: int
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ budget_tokens = self.budget_tokens
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "type": type_,
+ "budgetTokens": budget_tokens,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ type_ = cast(Literal["enabled"], d.pop("type"))
+ if type_ != "enabled":
+ raise ValueError(f"type must match const 'enabled', got '{type_}'")
+
+ budget_tokens = d.pop("budgetTokens")
+
+ anthropic_provider_options_anthropic_anthropic_thinking_enabled = cls(
+ type_=type_,
+ budget_tokens=budget_tokens,
+ )
+
+ return anthropic_provider_options_anthropic_anthropic_thinking_enabled
diff --git a/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic_anthropic_web_search.py b/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic_anthropic_web_search.py
new file mode 100644
index 0000000000..76ad01e1a8
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/anthropic_provider_options_anthropic_anthropic_web_search.py
@@ -0,0 +1,61 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class AnthropicProviderOptionsAnthropicAnthropicWebSearch:
+ """
+ Attributes:
+ allowed_domains (list[str] | Unset):
+ """
+
+ allowed_domains: list[str] | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ allowed_domains: list[str] | Unset = UNSET
+ if not isinstance(self.allowed_domains, Unset):
+ allowed_domains = self.allowed_domains
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if allowed_domains is not UNSET:
+ field_dict["allowedDomains"] = allowed_domains
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ allowed_domains = cast(list[str], d.pop("allowedDomains", UNSET))
+
+ anthropic_provider_options_anthropic_anthropic_web_search = cls(
+ allowed_domains=allowed_domains,
+ )
+
+ anthropic_provider_options_anthropic_anthropic_web_search.additional_properties = d
+ return anthropic_provider_options_anthropic_anthropic_web_search
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/authorization.py b/packages/liveblocks-python/liveblocks/models/authorization.py
new file mode 100644
index 0000000000..f4515e559d
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/authorization.py
@@ -0,0 +1,62 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class Authorization:
+ """
+ Example:
+ {'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi...'}
+
+ Attributes:
+ token (str | Unset):
+ """
+
+ token: str | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ token = self.token
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if token is not UNSET:
+ field_dict["token"] = token
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ token = d.pop("token", UNSET)
+
+ authorization = cls(
+ token=token,
+ )
+
+ authorization.additional_properties = d
+ return authorization
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/authorize_user_request_body.py b/packages/liveblocks-python/liveblocks/models/authorize_user_request_body.py
new file mode 100644
index 0000000000..0ada521944
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/authorize_user_request_body.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.authorize_user_request_body_permissions import AuthorizeUserRequestBodyPermissions
+ from ..models.authorize_user_request_body_user_info import AuthorizeUserRequestBodyUserInfo
+
+
+@_attrs_define
+class AuthorizeUserRequestBody:
+ """
+ Example:
+ {'userId': 'user-123', 'userInfo': {'name': 'bob', 'avatar': 'https://example.org/images/user123.jpg'},
+ 'organizationId': 'acme-corp', 'permissions': {'my-room-1': ['room:write'], 'my-room-2': ['room:write'], 'my-
+ room-*': ['room:read']}}
+
+ Attributes:
+ user_id (str):
+ permissions (AuthorizeUserRequestBodyPermissions):
+ user_info (AuthorizeUserRequestBodyUserInfo | Unset):
+ organization_id (str | Unset):
+ """
+
+ user_id: str
+ permissions: AuthorizeUserRequestBodyPermissions
+ user_info: AuthorizeUserRequestBodyUserInfo | Unset = UNSET
+ organization_id: str | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ permissions = self.permissions.to_dict()
+
+ user_info: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.user_info, Unset):
+ user_info = self.user_info.to_dict()
+
+ organization_id = self.organization_id
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ "permissions": permissions,
+ }
+ )
+ if user_info is not UNSET:
+ field_dict["userInfo"] = user_info
+ if organization_id is not UNSET:
+ field_dict["organizationId"] = organization_id
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.authorize_user_request_body_permissions import AuthorizeUserRequestBodyPermissions
+ from ..models.authorize_user_request_body_user_info import AuthorizeUserRequestBodyUserInfo
+
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ permissions = AuthorizeUserRequestBodyPermissions.from_dict(d.pop("permissions"))
+
+ _user_info = d.pop("userInfo", UNSET)
+ user_info: AuthorizeUserRequestBodyUserInfo | Unset
+ if isinstance(_user_info, Unset):
+ user_info = UNSET
+ else:
+ user_info = AuthorizeUserRequestBodyUserInfo.from_dict(_user_info)
+
+ organization_id = d.pop("organizationId", UNSET)
+
+ authorize_user_request_body = cls(
+ user_id=user_id,
+ permissions=permissions,
+ user_info=user_info,
+ organization_id=organization_id,
+ )
+
+ return authorize_user_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/authorize_user_request_body_permissions.py b/packages/liveblocks-python/liveblocks/models/authorize_user_request_body_permissions.py
new file mode 100644
index 0000000000..2fc5e0133c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/authorize_user_request_body_permissions.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class AuthorizeUserRequestBodyPermissions:
+ """ """
+
+ additional_properties: dict[str, list[str]] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ field_dict[prop_name] = prop
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ authorize_user_request_body_permissions = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+ additional_property = cast(list[str], prop_dict)
+
+ additional_properties[prop_name] = additional_property
+
+ authorize_user_request_body_permissions.additional_properties = additional_properties
+ return authorize_user_request_body_permissions
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> list[str]:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: list[str]) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/authorize_user_request_body_user_info.py b/packages/liveblocks-python/liveblocks/models/authorize_user_request_body_user_info.py
new file mode 100644
index 0000000000..1f3f46b38e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/authorize_user_request_body_user_info.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class AuthorizeUserRequestBodyUserInfo:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ authorize_user_request_body_user_info = cls()
+
+ authorize_user_request_body_user_info.additional_properties = d
+ return authorize_user_request_body_user_info
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/authorize_user_response.py b/packages/liveblocks-python/liveblocks/models/authorize_user_response.py
new file mode 100644
index 0000000000..320bbe3c48
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/authorize_user_response.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class AuthorizeUserResponse:
+ """
+ Example:
+ {'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi...'}
+
+ Attributes:
+ token (str):
+ """
+
+ token: str
+
+ def to_dict(self) -> dict[str, Any]:
+ token = self.token
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "token": token,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ token = d.pop("token")
+
+ authorize_user_response = cls(
+ token=token,
+ )
+
+ return authorize_user_response
diff --git a/packages/liveblocks-python/liveblocks/models/comment.py b/packages/liveblocks-python/liveblocks/models/comment.py
new file mode 100644
index 0000000000..a0bcabc9a1
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/comment.py
@@ -0,0 +1,206 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.comment_attachment import CommentAttachment
+ from ..models.comment_body import CommentBody
+ from ..models.comment_metadata import CommentMetadata
+ from ..models.comment_reaction import CommentReaction
+
+
+@_attrs_define
+class Comment:
+ """
+ Attributes:
+ type_ (Literal['comment']):
+ thread_id (str):
+ room_id (str):
+ id (str):
+ user_id (str):
+ created_at (datetime.datetime):
+ metadata (CommentMetadata): Custom metadata attached to a comment. Supports maximum 50 entries. Key length has a
+ limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+ reactions (list[CommentReaction]):
+ attachments (list[CommentAttachment]):
+ edited_at (datetime.datetime | Unset):
+ deleted_at (datetime.datetime | Unset):
+ body (CommentBody | Unset): Example: {'version': 1, 'content': [{'type': 'paragraph', 'children': [{'text':
+ 'Hello '}, {'text': 'world', 'bold': True}]}]}.
+ """
+
+ type_: Literal["comment"]
+ thread_id: str
+ room_id: str
+ id: str
+ user_id: str
+ created_at: datetime.datetime
+ metadata: CommentMetadata
+ reactions: list[CommentReaction]
+ attachments: list[CommentAttachment]
+ edited_at: datetime.datetime | Unset = UNSET
+ deleted_at: datetime.datetime | Unset = UNSET
+ body: CommentBody | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ thread_id = self.thread_id
+
+ room_id = self.room_id
+
+ id = self.id
+
+ user_id = self.user_id
+
+ created_at = self.created_at.isoformat()
+
+ metadata = self.metadata.to_dict()
+
+ reactions = []
+ for reactions_item_data in self.reactions:
+ reactions_item = reactions_item_data.to_dict()
+ reactions.append(reactions_item)
+
+ attachments = []
+ for attachments_item_data in self.attachments:
+ attachments_item = attachments_item_data.to_dict()
+ attachments.append(attachments_item)
+
+ edited_at: str | Unset = UNSET
+ if not isinstance(self.edited_at, Unset):
+ edited_at = self.edited_at.isoformat()
+
+ deleted_at: str | Unset = UNSET
+ if not isinstance(self.deleted_at, Unset):
+ deleted_at = self.deleted_at.isoformat()
+
+ body: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.body, Unset):
+ body = self.body.to_dict()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "type": type_,
+ "threadId": thread_id,
+ "roomId": room_id,
+ "id": id,
+ "userId": user_id,
+ "createdAt": created_at,
+ "metadata": metadata,
+ "reactions": reactions,
+ "attachments": attachments,
+ }
+ )
+ if edited_at is not UNSET:
+ field_dict["editedAt"] = edited_at
+ if deleted_at is not UNSET:
+ field_dict["deletedAt"] = deleted_at
+ if body is not UNSET:
+ field_dict["body"] = body
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.comment_attachment import CommentAttachment
+ from ..models.comment_body import CommentBody
+ from ..models.comment_metadata import CommentMetadata
+ from ..models.comment_reaction import CommentReaction
+
+ d = dict(src_dict)
+ type_ = cast(Literal["comment"], d.pop("type"))
+ if type_ != "comment":
+ raise ValueError(f"type must match const 'comment', got '{type_}'")
+
+ thread_id = d.pop("threadId")
+
+ room_id = d.pop("roomId")
+
+ id = d.pop("id")
+
+ user_id = d.pop("userId")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ metadata = CommentMetadata.from_dict(d.pop("metadata"))
+
+ reactions = []
+ _reactions = d.pop("reactions")
+ for reactions_item_data in _reactions:
+ reactions_item = CommentReaction.from_dict(reactions_item_data)
+
+ reactions.append(reactions_item)
+
+ attachments = []
+ _attachments = d.pop("attachments")
+ for attachments_item_data in _attachments:
+ attachments_item = CommentAttachment.from_dict(attachments_item_data)
+
+ attachments.append(attachments_item)
+
+ _edited_at = d.pop("editedAt", UNSET)
+ edited_at: datetime.datetime | Unset
+ if isinstance(_edited_at, Unset):
+ edited_at = UNSET
+ else:
+ edited_at = isoparse(_edited_at)
+
+ _deleted_at = d.pop("deletedAt", UNSET)
+ deleted_at: datetime.datetime | Unset
+ if isinstance(_deleted_at, Unset):
+ deleted_at = UNSET
+ else:
+ deleted_at = isoparse(_deleted_at)
+
+ _body = d.pop("body", UNSET)
+ body: CommentBody | Unset
+ if isinstance(_body, Unset):
+ body = UNSET
+ else:
+ body = CommentBody.from_dict(_body)
+
+ comment = cls(
+ type_=type_,
+ thread_id=thread_id,
+ room_id=room_id,
+ id=id,
+ user_id=user_id,
+ created_at=created_at,
+ metadata=metadata,
+ reactions=reactions,
+ attachments=attachments,
+ edited_at=edited_at,
+ deleted_at=deleted_at,
+ body=body,
+ )
+
+ comment.additional_properties = d
+ return comment
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/comment_attachment.py b/packages/liveblocks-python/liveblocks/models/comment_attachment.py
new file mode 100644
index 0000000000..e7b6a88136
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/comment_attachment.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class CommentAttachment:
+ """
+ Example:
+ {'type': 'attachment', 'id': 'at_abc123', 'mimeType': 'image/png', 'name': 'screenshot.png', 'size': 12345}
+
+ Attributes:
+ type_ (Literal['attachment']):
+ id (str):
+ mime_type (str):
+ name (str):
+ size (int):
+ """
+
+ type_: Literal["attachment"]
+ id: str
+ mime_type: str
+ name: str
+ size: int
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ id = self.id
+
+ mime_type = self.mime_type
+
+ name = self.name
+
+ size = self.size
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "type": type_,
+ "id": id,
+ "mimeType": mime_type,
+ "name": name,
+ "size": size,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ type_ = cast(Literal["attachment"], d.pop("type"))
+ if type_ != "attachment":
+ raise ValueError(f"type must match const 'attachment', got '{type_}'")
+
+ id = d.pop("id")
+
+ mime_type = d.pop("mimeType")
+
+ name = d.pop("name")
+
+ size = d.pop("size")
+
+ comment_attachment = cls(
+ type_=type_,
+ id=id,
+ mime_type=mime_type,
+ name=name,
+ size=size,
+ )
+
+ return comment_attachment
diff --git a/packages/liveblocks-python/liveblocks/models/comment_body.py b/packages/liveblocks-python/liveblocks/models/comment_body.py
new file mode 100644
index 0000000000..d49ed6043b
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/comment_body.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.comment_body_content_item import CommentBodyContentItem
+
+
+@_attrs_define
+class CommentBody:
+ """
+ Example:
+ {'version': 1, 'content': [{'type': 'paragraph', 'children': [{'text': 'Hello '}, {'text': 'world', 'bold':
+ True}]}]}
+
+ Attributes:
+ version (int):
+ content (list[CommentBodyContentItem]):
+ """
+
+ version: int
+ content: list[CommentBodyContentItem]
+
+ def to_dict(self) -> dict[str, Any]:
+ version = self.version
+
+ content = []
+ for content_item_data in self.content:
+ content_item = content_item_data.to_dict()
+ content.append(content_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "version": version,
+ "content": content,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.comment_body_content_item import CommentBodyContentItem
+
+ d = dict(src_dict)
+ version = d.pop("version")
+
+ content = []
+ _content = d.pop("content")
+ for content_item_data in _content:
+ content_item = CommentBodyContentItem.from_dict(content_item_data)
+
+ content.append(content_item)
+
+ comment_body = cls(
+ version=version,
+ content=content,
+ )
+
+ return comment_body
diff --git a/packages/liveblocks-python/liveblocks/models/comment_body_content_item.py b/packages/liveblocks-python/liveblocks/models/comment_body_content_item.py
new file mode 100644
index 0000000000..39b220e0c5
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/comment_body_content_item.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class CommentBodyContentItem:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ comment_body_content_item = cls()
+
+ comment_body_content_item.additional_properties = d
+ return comment_body_content_item
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/comment_metadata.py b/packages/liveblocks-python/liveblocks/models/comment_metadata.py
new file mode 100644
index 0000000000..41f651c6fc
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/comment_metadata.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class CommentMetadata:
+ """Custom metadata attached to a comment. Supports maximum 50 entries. Key length has a limit of 40 characters maximum.
+ Value length has a limit of 4000 characters maximum for strings.
+
+ """
+
+ additional_properties: dict[str, bool | float | str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ field_dict[prop_name] = prop
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ comment_metadata = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+
+ def _parse_additional_property(data: object) -> bool | float | str:
+ return cast(bool | float | str, data)
+
+ additional_property = _parse_additional_property(prop_dict)
+
+ additional_properties[prop_name] = additional_property
+
+ comment_metadata.additional_properties = additional_properties
+ return comment_metadata
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> bool | float | str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: bool | float | str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/comment_reaction.py b/packages/liveblocks-python/liveblocks/models/comment_reaction.py
new file mode 100644
index 0000000000..eae007eef7
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/comment_reaction.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+
+@_attrs_define
+class CommentReaction:
+ """
+ Attributes:
+ user_id (str):
+ created_at (datetime.datetime):
+ emoji (str):
+ """
+
+ user_id: str
+ created_at: datetime.datetime
+ emoji: str
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ created_at = self.created_at.isoformat()
+
+ emoji = self.emoji
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ "createdAt": created_at,
+ "emoji": emoji,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ emoji = d.pop("emoji")
+
+ comment_reaction = cls(
+ user_id=user_id,
+ created_at=created_at,
+ emoji=emoji,
+ )
+
+ return comment_reaction
diff --git a/packages/liveblocks-python/liveblocks/models/copy_json_patch_operation.py b/packages/liveblocks-python/liveblocks/models/copy_json_patch_operation.py
new file mode 100644
index 0000000000..fb38742571
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/copy_json_patch_operation.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class CopyJsonPatchOperation:
+ """
+ Attributes:
+ op (Literal['copy']):
+ from_ (str): A JSON Pointer to the source location (RFC 6901). Must start with "/".
+ path (str): A JSON Pointer to the target location (RFC 6901). Must start with "/".
+ """
+
+ op: Literal["copy"]
+ from_: str
+ path: str
+
+ def to_dict(self) -> dict[str, Any]:
+ op = self.op
+
+ from_ = self.from_
+
+ path = self.path
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "op": op,
+ "from": from_,
+ "path": path,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ op = cast(Literal["copy"], d.pop("op"))
+ if op != "copy":
+ raise ValueError(f"op must match const 'copy', got '{op}'")
+
+ from_ = d.pop("from")
+
+ path = d.pop("path")
+
+ copy_json_patch_operation = cls(
+ op=op,
+ from_=from_,
+ path=path,
+ )
+
+ return copy_json_patch_operation
diff --git a/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_anthropic.py b/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_anthropic.py
new file mode 100644
index 0000000000..4a03e0def3
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_anthropic.py
@@ -0,0 +1,167 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..models.anthropic_model import AnthropicModel
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.anthropic_provider_options import AnthropicProviderOptions
+
+
+@_attrs_define
+class CreateAiCopilotOptionsAnthropic:
+ """
+ Example:
+ {'name': 'My Anthropic Copilot', 'systemPrompt': 'You are a helpful assistant.', 'providerApiKey': 'sk-ant-...',
+ 'provider': 'anthropic', 'providerModel': 'claude-3-5-sonnet-latest'}
+
+ Attributes:
+ name (str):
+ system_prompt (str):
+ provider_api_key (str):
+ provider (Literal['anthropic']):
+ provider_model (AnthropicModel): Example: claude-3-5-sonnet-latest.
+ description (str | Unset):
+ knowledge_prompt (str | Unset):
+ always_use_knowledge (bool | Unset):
+ settings (AiCopilotProviderSettings | Unset): Example: {'maxTokens': 4096, 'temperature': 0.7, 'topP': 0.9}.
+ provider_options (AnthropicProviderOptions | Unset): Example: {'anthropic': {'thinking': {'type': 'enabled',
+ 'budgetTokens': 10000}}}.
+ """
+
+ name: str
+ system_prompt: str
+ provider_api_key: str
+ provider: Literal["anthropic"]
+ provider_model: AnthropicModel
+ description: str | Unset = UNSET
+ knowledge_prompt: str | Unset = UNSET
+ always_use_knowledge: bool | Unset = UNSET
+ settings: AiCopilotProviderSettings | Unset = UNSET
+ provider_options: AnthropicProviderOptions | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ name = self.name
+
+ system_prompt = self.system_prompt
+
+ provider_api_key = self.provider_api_key
+
+ provider = self.provider
+
+ provider_model = self.provider_model.value
+
+ description = self.description
+
+ knowledge_prompt = self.knowledge_prompt
+
+ always_use_knowledge = self.always_use_knowledge
+
+ settings: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.settings, Unset):
+ settings = self.settings.to_dict()
+
+ provider_options: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.provider_options, Unset):
+ provider_options = self.provider_options.to_dict()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "name": name,
+ "systemPrompt": system_prompt,
+ "providerApiKey": provider_api_key,
+ "provider": provider,
+ "providerModel": provider_model,
+ }
+ )
+ if description is not UNSET:
+ field_dict["description"] = description
+ if knowledge_prompt is not UNSET:
+ field_dict["knowledgePrompt"] = knowledge_prompt
+ if always_use_knowledge is not UNSET:
+ field_dict["alwaysUseKnowledge"] = always_use_knowledge
+ if settings is not UNSET:
+ field_dict["settings"] = settings
+ if provider_options is not UNSET:
+ field_dict["providerOptions"] = provider_options
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.anthropic_provider_options import AnthropicProviderOptions
+
+ d = dict(src_dict)
+ name = d.pop("name")
+
+ system_prompt = d.pop("systemPrompt")
+
+ provider_api_key = d.pop("providerApiKey")
+
+ provider = cast(Literal["anthropic"], d.pop("provider"))
+ if provider != "anthropic":
+ raise ValueError(f"provider must match const 'anthropic', got '{provider}'")
+
+ provider_model = AnthropicModel(d.pop("providerModel"))
+
+ description = d.pop("description", UNSET)
+
+ knowledge_prompt = d.pop("knowledgePrompt", UNSET)
+
+ always_use_knowledge = d.pop("alwaysUseKnowledge", UNSET)
+
+ _settings = d.pop("settings", UNSET)
+ settings: AiCopilotProviderSettings | Unset
+ if isinstance(_settings, Unset):
+ settings = UNSET
+ else:
+ settings = AiCopilotProviderSettings.from_dict(_settings)
+
+ _provider_options = d.pop("providerOptions", UNSET)
+ provider_options: AnthropicProviderOptions | Unset
+ if isinstance(_provider_options, Unset):
+ provider_options = UNSET
+ else:
+ provider_options = AnthropicProviderOptions.from_dict(_provider_options)
+
+ create_ai_copilot_options_anthropic = cls(
+ name=name,
+ system_prompt=system_prompt,
+ provider_api_key=provider_api_key,
+ provider=provider,
+ provider_model=provider_model,
+ description=description,
+ knowledge_prompt=knowledge_prompt,
+ always_use_knowledge=always_use_knowledge,
+ settings=settings,
+ provider_options=provider_options,
+ )
+
+ create_ai_copilot_options_anthropic.additional_properties = d
+ return create_ai_copilot_options_anthropic
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_base.py b/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_base.py
new file mode 100644
index 0000000000..ce36b51860
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_base.py
@@ -0,0 +1,110 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+
+
+@_attrs_define
+class CreateAiCopilotOptionsBase:
+ """
+ Example:
+ {'name': 'My Copilot', 'systemPrompt': 'You are a helpful assistant.', 'providerApiKey': 'sk-...',
+ 'alwaysUseKnowledge': True, 'settings': {'maxTokens': 4096, 'temperature': 0.7}}
+
+ Attributes:
+ name (str):
+ system_prompt (str):
+ provider_api_key (str):
+ description (str | Unset):
+ knowledge_prompt (str | Unset):
+ always_use_knowledge (bool | Unset):
+ settings (AiCopilotProviderSettings | Unset): Example: {'maxTokens': 4096, 'temperature': 0.7, 'topP': 0.9}.
+ """
+
+ name: str
+ system_prompt: str
+ provider_api_key: str
+ description: str | Unset = UNSET
+ knowledge_prompt: str | Unset = UNSET
+ always_use_knowledge: bool | Unset = UNSET
+ settings: AiCopilotProviderSettings | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ name = self.name
+
+ system_prompt = self.system_prompt
+
+ provider_api_key = self.provider_api_key
+
+ description = self.description
+
+ knowledge_prompt = self.knowledge_prompt
+
+ always_use_knowledge = self.always_use_knowledge
+
+ settings: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.settings, Unset):
+ settings = self.settings.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "name": name,
+ "systemPrompt": system_prompt,
+ "providerApiKey": provider_api_key,
+ }
+ )
+ if description is not UNSET:
+ field_dict["description"] = description
+ if knowledge_prompt is not UNSET:
+ field_dict["knowledgePrompt"] = knowledge_prompt
+ if always_use_knowledge is not UNSET:
+ field_dict["alwaysUseKnowledge"] = always_use_knowledge
+ if settings is not UNSET:
+ field_dict["settings"] = settings
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+
+ d = dict(src_dict)
+ name = d.pop("name")
+
+ system_prompt = d.pop("systemPrompt")
+
+ provider_api_key = d.pop("providerApiKey")
+
+ description = d.pop("description", UNSET)
+
+ knowledge_prompt = d.pop("knowledgePrompt", UNSET)
+
+ always_use_knowledge = d.pop("alwaysUseKnowledge", UNSET)
+
+ _settings = d.pop("settings", UNSET)
+ settings: AiCopilotProviderSettings | Unset
+ if isinstance(_settings, Unset):
+ settings = UNSET
+ else:
+ settings = AiCopilotProviderSettings.from_dict(_settings)
+
+ create_ai_copilot_options_base = cls(
+ name=name,
+ system_prompt=system_prompt,
+ provider_api_key=provider_api_key,
+ description=description,
+ knowledge_prompt=knowledge_prompt,
+ always_use_knowledge=always_use_knowledge,
+ settings=settings,
+ )
+
+ return create_ai_copilot_options_base
diff --git a/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_google.py b/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_google.py
new file mode 100644
index 0000000000..932a4289ca
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_google.py
@@ -0,0 +1,167 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..models.google_model import GoogleModel
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.google_provider_options import GoogleProviderOptions
+
+
+@_attrs_define
+class CreateAiCopilotOptionsGoogle:
+ """
+ Example:
+ {'name': 'My Google Copilot', 'systemPrompt': 'You are a helpful assistant.', 'providerApiKey': 'AIza...',
+ 'provider': 'google', 'providerModel': 'gemini-2.5-flash'}
+
+ Attributes:
+ name (str):
+ system_prompt (str):
+ provider_api_key (str):
+ provider (Literal['google']):
+ provider_model (GoogleModel): Example: gemini-2.5-flash.
+ description (str | Unset):
+ knowledge_prompt (str | Unset):
+ always_use_knowledge (bool | Unset):
+ settings (AiCopilotProviderSettings | Unset): Example: {'maxTokens': 4096, 'temperature': 0.7, 'topP': 0.9}.
+ provider_options (GoogleProviderOptions | Unset): Example: {'google': {'thinkingConfig': {'thinkingBudget':
+ 10000}}}.
+ """
+
+ name: str
+ system_prompt: str
+ provider_api_key: str
+ provider: Literal["google"]
+ provider_model: GoogleModel
+ description: str | Unset = UNSET
+ knowledge_prompt: str | Unset = UNSET
+ always_use_knowledge: bool | Unset = UNSET
+ settings: AiCopilotProviderSettings | Unset = UNSET
+ provider_options: GoogleProviderOptions | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ name = self.name
+
+ system_prompt = self.system_prompt
+
+ provider_api_key = self.provider_api_key
+
+ provider = self.provider
+
+ provider_model = self.provider_model.value
+
+ description = self.description
+
+ knowledge_prompt = self.knowledge_prompt
+
+ always_use_knowledge = self.always_use_knowledge
+
+ settings: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.settings, Unset):
+ settings = self.settings.to_dict()
+
+ provider_options: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.provider_options, Unset):
+ provider_options = self.provider_options.to_dict()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "name": name,
+ "systemPrompt": system_prompt,
+ "providerApiKey": provider_api_key,
+ "provider": provider,
+ "providerModel": provider_model,
+ }
+ )
+ if description is not UNSET:
+ field_dict["description"] = description
+ if knowledge_prompt is not UNSET:
+ field_dict["knowledgePrompt"] = knowledge_prompt
+ if always_use_knowledge is not UNSET:
+ field_dict["alwaysUseKnowledge"] = always_use_knowledge
+ if settings is not UNSET:
+ field_dict["settings"] = settings
+ if provider_options is not UNSET:
+ field_dict["providerOptions"] = provider_options
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.google_provider_options import GoogleProviderOptions
+
+ d = dict(src_dict)
+ name = d.pop("name")
+
+ system_prompt = d.pop("systemPrompt")
+
+ provider_api_key = d.pop("providerApiKey")
+
+ provider = cast(Literal["google"], d.pop("provider"))
+ if provider != "google":
+ raise ValueError(f"provider must match const 'google', got '{provider}'")
+
+ provider_model = GoogleModel(d.pop("providerModel"))
+
+ description = d.pop("description", UNSET)
+
+ knowledge_prompt = d.pop("knowledgePrompt", UNSET)
+
+ always_use_knowledge = d.pop("alwaysUseKnowledge", UNSET)
+
+ _settings = d.pop("settings", UNSET)
+ settings: AiCopilotProviderSettings | Unset
+ if isinstance(_settings, Unset):
+ settings = UNSET
+ else:
+ settings = AiCopilotProviderSettings.from_dict(_settings)
+
+ _provider_options = d.pop("providerOptions", UNSET)
+ provider_options: GoogleProviderOptions | Unset
+ if isinstance(_provider_options, Unset):
+ provider_options = UNSET
+ else:
+ provider_options = GoogleProviderOptions.from_dict(_provider_options)
+
+ create_ai_copilot_options_google = cls(
+ name=name,
+ system_prompt=system_prompt,
+ provider_api_key=provider_api_key,
+ provider=provider,
+ provider_model=provider_model,
+ description=description,
+ knowledge_prompt=knowledge_prompt,
+ always_use_knowledge=always_use_knowledge,
+ settings=settings,
+ provider_options=provider_options,
+ )
+
+ create_ai_copilot_options_google.additional_properties = d
+ return create_ai_copilot_options_google
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_open_ai.py b/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_open_ai.py
new file mode 100644
index 0000000000..8cab697b35
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_open_ai.py
@@ -0,0 +1,147 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+
+from ..models.open_ai_model import OpenAiModel
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.open_ai_provider_options import OpenAiProviderOptions
+
+
+@_attrs_define
+class CreateAiCopilotOptionsOpenAi:
+ """
+ Example:
+ {'name': 'My Copilot', 'systemPrompt': 'You are a helpful assistant.', 'providerApiKey': 'sk-...', 'provider':
+ 'openai', 'providerModel': 'gpt-4o'}
+
+ Attributes:
+ name (str):
+ system_prompt (str):
+ provider_api_key (str):
+ provider (Literal['openai']):
+ provider_model (OpenAiModel): Example: gpt-4o.
+ description (str | Unset):
+ knowledge_prompt (str | Unset):
+ always_use_knowledge (bool | Unset):
+ settings (AiCopilotProviderSettings | Unset): Example: {'maxTokens': 4096, 'temperature': 0.7, 'topP': 0.9}.
+ provider_options (OpenAiProviderOptions | Unset): Example: {'openai': {'reasoningEffort': 'medium'}}.
+ """
+
+ name: str
+ system_prompt: str
+ provider_api_key: str
+ provider: Literal["openai"]
+ provider_model: OpenAiModel
+ description: str | Unset = UNSET
+ knowledge_prompt: str | Unset = UNSET
+ always_use_knowledge: bool | Unset = UNSET
+ settings: AiCopilotProviderSettings | Unset = UNSET
+ provider_options: OpenAiProviderOptions | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ name = self.name
+
+ system_prompt = self.system_prompt
+
+ provider_api_key = self.provider_api_key
+
+ provider = self.provider
+
+ provider_model = self.provider_model.value
+
+ description = self.description
+
+ knowledge_prompt = self.knowledge_prompt
+
+ always_use_knowledge = self.always_use_knowledge
+
+ settings: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.settings, Unset):
+ settings = self.settings.to_dict()
+
+ provider_options: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.provider_options, Unset):
+ provider_options = self.provider_options.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "name": name,
+ "systemPrompt": system_prompt,
+ "providerApiKey": provider_api_key,
+ "provider": provider,
+ "providerModel": provider_model,
+ }
+ )
+ if description is not UNSET:
+ field_dict["description"] = description
+ if knowledge_prompt is not UNSET:
+ field_dict["knowledgePrompt"] = knowledge_prompt
+ if always_use_knowledge is not UNSET:
+ field_dict["alwaysUseKnowledge"] = always_use_knowledge
+ if settings is not UNSET:
+ field_dict["settings"] = settings
+ if provider_options is not UNSET:
+ field_dict["providerOptions"] = provider_options
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.open_ai_provider_options import OpenAiProviderOptions
+
+ d = dict(src_dict)
+ name = d.pop("name")
+
+ system_prompt = d.pop("systemPrompt")
+
+ provider_api_key = d.pop("providerApiKey")
+
+ provider = cast(Literal["openai"], d.pop("provider"))
+ if provider != "openai":
+ raise ValueError(f"provider must match const 'openai', got '{provider}'")
+
+ provider_model = OpenAiModel(d.pop("providerModel"))
+
+ description = d.pop("description", UNSET)
+
+ knowledge_prompt = d.pop("knowledgePrompt", UNSET)
+
+ always_use_knowledge = d.pop("alwaysUseKnowledge", UNSET)
+
+ _settings = d.pop("settings", UNSET)
+ settings: AiCopilotProviderSettings | Unset
+ if isinstance(_settings, Unset):
+ settings = UNSET
+ else:
+ settings = AiCopilotProviderSettings.from_dict(_settings)
+
+ _provider_options = d.pop("providerOptions", UNSET)
+ provider_options: OpenAiProviderOptions | Unset
+ if isinstance(_provider_options, Unset):
+ provider_options = UNSET
+ else:
+ provider_options = OpenAiProviderOptions.from_dict(_provider_options)
+
+ create_ai_copilot_options_open_ai = cls(
+ name=name,
+ system_prompt=system_prompt,
+ provider_api_key=provider_api_key,
+ provider=provider,
+ provider_model=provider_model,
+ description=description,
+ knowledge_prompt=knowledge_prompt,
+ always_use_knowledge=always_use_knowledge,
+ settings=settings,
+ provider_options=provider_options,
+ )
+
+ return create_ai_copilot_options_open_ai
diff --git a/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_open_ai_compatible.py b/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_open_ai_compatible.py
new file mode 100644
index 0000000000..b153d327d0
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_ai_copilot_options_open_ai_compatible.py
@@ -0,0 +1,164 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+
+
+@_attrs_define
+class CreateAiCopilotOptionsOpenAiCompatible:
+ """
+ Example:
+ {'name': 'My Compatible Copilot', 'systemPrompt': 'You are a helpful assistant.', 'providerApiKey': 'sk-...',
+ 'provider': 'openai-compatible', 'providerModel': 'my-custom-model', 'compatibleProviderName': 'my-provider',
+ 'providerBaseUrl': 'https://api.my-provider.com/v1'}
+
+ Attributes:
+ name (str):
+ system_prompt (str):
+ provider_api_key (str):
+ provider (Literal['openai-compatible']):
+ provider_model (str):
+ compatible_provider_name (str):
+ provider_base_url (str):
+ description (str | Unset):
+ knowledge_prompt (str | Unset):
+ always_use_knowledge (bool | Unset):
+ settings (AiCopilotProviderSettings | Unset): Example: {'maxTokens': 4096, 'temperature': 0.7, 'topP': 0.9}.
+ """
+
+ name: str
+ system_prompt: str
+ provider_api_key: str
+ provider: Literal["openai-compatible"]
+ provider_model: str
+ compatible_provider_name: str
+ provider_base_url: str
+ description: str | Unset = UNSET
+ knowledge_prompt: str | Unset = UNSET
+ always_use_knowledge: bool | Unset = UNSET
+ settings: AiCopilotProviderSettings | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ name = self.name
+
+ system_prompt = self.system_prompt
+
+ provider_api_key = self.provider_api_key
+
+ provider = self.provider
+
+ provider_model = self.provider_model
+
+ compatible_provider_name = self.compatible_provider_name
+
+ provider_base_url = self.provider_base_url
+
+ description = self.description
+
+ knowledge_prompt = self.knowledge_prompt
+
+ always_use_knowledge = self.always_use_knowledge
+
+ settings: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.settings, Unset):
+ settings = self.settings.to_dict()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "name": name,
+ "systemPrompt": system_prompt,
+ "providerApiKey": provider_api_key,
+ "provider": provider,
+ "providerModel": provider_model,
+ "compatibleProviderName": compatible_provider_name,
+ "providerBaseUrl": provider_base_url,
+ }
+ )
+ if description is not UNSET:
+ field_dict["description"] = description
+ if knowledge_prompt is not UNSET:
+ field_dict["knowledgePrompt"] = knowledge_prompt
+ if always_use_knowledge is not UNSET:
+ field_dict["alwaysUseKnowledge"] = always_use_knowledge
+ if settings is not UNSET:
+ field_dict["settings"] = settings
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+
+ d = dict(src_dict)
+ name = d.pop("name")
+
+ system_prompt = d.pop("systemPrompt")
+
+ provider_api_key = d.pop("providerApiKey")
+
+ provider = cast(Literal["openai-compatible"], d.pop("provider"))
+ if provider != "openai-compatible":
+ raise ValueError(f"provider must match const 'openai-compatible', got '{provider}'")
+
+ provider_model = d.pop("providerModel")
+
+ compatible_provider_name = d.pop("compatibleProviderName")
+
+ provider_base_url = d.pop("providerBaseUrl")
+
+ description = d.pop("description", UNSET)
+
+ knowledge_prompt = d.pop("knowledgePrompt", UNSET)
+
+ always_use_knowledge = d.pop("alwaysUseKnowledge", UNSET)
+
+ _settings = d.pop("settings", UNSET)
+ settings: AiCopilotProviderSettings | Unset
+ if isinstance(_settings, Unset):
+ settings = UNSET
+ else:
+ settings = AiCopilotProviderSettings.from_dict(_settings)
+
+ create_ai_copilot_options_open_ai_compatible = cls(
+ name=name,
+ system_prompt=system_prompt,
+ provider_api_key=provider_api_key,
+ provider=provider,
+ provider_model=provider_model,
+ compatible_provider_name=compatible_provider_name,
+ provider_base_url=provider_base_url,
+ description=description,
+ knowledge_prompt=knowledge_prompt,
+ always_use_knowledge=always_use_knowledge,
+ settings=settings,
+ )
+
+ create_ai_copilot_options_open_ai_compatible.additional_properties = d
+ return create_ai_copilot_options_open_ai_compatible
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/create_comment_request_body.py b/packages/liveblocks-python/liveblocks/models/create_comment_request_body.py
new file mode 100644
index 0000000000..6c48d36e1d
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_comment_request_body.py
@@ -0,0 +1,104 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.comment_body import CommentBody
+ from ..models.comment_metadata import CommentMetadata
+
+
+@_attrs_define
+class CreateCommentRequestBody:
+ """
+ Attributes:
+ user_id (str):
+ body (CommentBody): Example: {'version': 1, 'content': [{'type': 'paragraph', 'children': [{'text': 'Hello '},
+ {'text': 'world', 'bold': True}]}]}.
+ created_at (datetime.datetime | Unset):
+ metadata (CommentMetadata | Unset): Custom metadata attached to a comment. Supports maximum 50 entries. Key
+ length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+ attachment_ids (list[str] | Unset):
+ """
+
+ user_id: str
+ body: CommentBody
+ created_at: datetime.datetime | Unset = UNSET
+ metadata: CommentMetadata | Unset = UNSET
+ attachment_ids: list[str] | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ body = self.body.to_dict()
+
+ created_at: str | Unset = UNSET
+ if not isinstance(self.created_at, Unset):
+ created_at = self.created_at.isoformat()
+
+ metadata: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.metadata, Unset):
+ metadata = self.metadata.to_dict()
+
+ attachment_ids: list[str] | Unset = UNSET
+ if not isinstance(self.attachment_ids, Unset):
+ attachment_ids = self.attachment_ids
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ "body": body,
+ }
+ )
+ if created_at is not UNSET:
+ field_dict["createdAt"] = created_at
+ if metadata is not UNSET:
+ field_dict["metadata"] = metadata
+ if attachment_ids is not UNSET:
+ field_dict["attachmentIds"] = attachment_ids
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.comment_body import CommentBody
+ from ..models.comment_metadata import CommentMetadata
+
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ body = CommentBody.from_dict(d.pop("body"))
+
+ _created_at = d.pop("createdAt", UNSET)
+ created_at: datetime.datetime | Unset
+ if isinstance(_created_at, Unset):
+ created_at = UNSET
+ else:
+ created_at = isoparse(_created_at)
+
+ _metadata = d.pop("metadata", UNSET)
+ metadata: CommentMetadata | Unset
+ if isinstance(_metadata, Unset):
+ metadata = UNSET
+ else:
+ metadata = CommentMetadata.from_dict(_metadata)
+
+ attachment_ids = cast(list[str], d.pop("attachmentIds", UNSET))
+
+ create_comment_request_body = cls(
+ user_id=user_id,
+ body=body,
+ created_at=created_at,
+ metadata=metadata,
+ attachment_ids=attachment_ids,
+ )
+
+ return create_comment_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/create_file_knowledge_source_response_200.py b/packages/liveblocks-python/liveblocks/models/create_file_knowledge_source_response_200.py
new file mode 100644
index 0000000000..173fb0cd3b
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_file_knowledge_source_response_200.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class CreateFileKnowledgeSourceResponse200:
+ """
+ Attributes:
+ id (str | Unset):
+ """
+
+ id: str | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if id is not UNSET:
+ field_dict["id"] = id
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ id = d.pop("id", UNSET)
+
+ create_file_knowledge_source_response_200 = cls(
+ id=id,
+ )
+
+ create_file_knowledge_source_response_200.additional_properties = d
+ return create_file_knowledge_source_response_200
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/create_group_request_body.py b/packages/liveblocks-python/liveblocks/models/create_group_request_body.py
new file mode 100644
index 0000000000..132ba159ab
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_group_request_body.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.create_group_request_body_scopes import CreateGroupRequestBodyScopes
+
+
+@_attrs_define
+class CreateGroupRequestBody:
+ """
+ Example:
+ {'id': 'engineering', 'memberIds': ['alice', 'bob'], 'organizationId': 'org_123456789', 'scopes': {'mention':
+ True}}
+
+ Attributes:
+ id (str):
+ member_ids (list[str] | Unset):
+ organization_id (str | Unset):
+ scopes (CreateGroupRequestBodyScopes | Unset):
+ """
+
+ id: str
+ member_ids: list[str] | Unset = UNSET
+ organization_id: str | Unset = UNSET
+ scopes: CreateGroupRequestBodyScopes | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ member_ids: list[str] | Unset = UNSET
+ if not isinstance(self.member_ids, Unset):
+ member_ids = self.member_ids
+
+ organization_id = self.organization_id
+
+ scopes: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.scopes, Unset):
+ scopes = self.scopes.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ }
+ )
+ if member_ids is not UNSET:
+ field_dict["memberIds"] = member_ids
+ if organization_id is not UNSET:
+ field_dict["organizationId"] = organization_id
+ if scopes is not UNSET:
+ field_dict["scopes"] = scopes
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.create_group_request_body_scopes import CreateGroupRequestBodyScopes
+
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ member_ids = cast(list[str], d.pop("memberIds", UNSET))
+
+ organization_id = d.pop("organizationId", UNSET)
+
+ _scopes = d.pop("scopes", UNSET)
+ scopes: CreateGroupRequestBodyScopes | Unset
+ if isinstance(_scopes, Unset):
+ scopes = UNSET
+ else:
+ scopes = CreateGroupRequestBodyScopes.from_dict(_scopes)
+
+ create_group_request_body = cls(
+ id=id,
+ member_ids=member_ids,
+ organization_id=organization_id,
+ scopes=scopes,
+ )
+
+ return create_group_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/create_group_request_body_scopes.py b/packages/liveblocks-python/liveblocks/models/create_group_request_body_scopes.py
new file mode 100644
index 0000000000..03d99be934
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_group_request_body_scopes.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class CreateGroupRequestBodyScopes:
+ """
+ Attributes:
+ mention (bool | Unset):
+ """
+
+ mention: bool | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ mention = self.mention
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if mention is not UNSET:
+ field_dict["mention"] = mention
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ mention = d.pop("mention", UNSET)
+
+ create_group_request_body_scopes = cls(
+ mention=mention,
+ )
+
+ create_group_request_body_scopes.additional_properties = d
+ return create_group_request_body_scopes
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/create_management_project_request_body.py b/packages/liveblocks-python/liveblocks/models/create_management_project_request_body.py
new file mode 100644
index 0000000000..a099d2588a
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_management_project_request_body.py
@@ -0,0 +1,92 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..models.management_project_type import ManagementProjectType
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class CreateManagementProjectRequestBody:
+ """
+ Example:
+ {'name': 'My Project', 'type': 'dev', 'versionCreationTimeout': False}
+
+ Attributes:
+ type_ (ManagementProjectType): Example: dev.
+ name (str | Unset):
+ version_creation_timeout (bool | int | Unset): False to disable timeout or number of seconds between 30 and 300.
+ """
+
+ type_: ManagementProjectType
+ name: str | Unset = UNSET
+ version_creation_timeout: bool | int | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_.value
+
+ name = self.name
+
+ version_creation_timeout: bool | int | Unset
+ if isinstance(self.version_creation_timeout, Unset):
+ version_creation_timeout = UNSET
+ else:
+ version_creation_timeout = self.version_creation_timeout
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "type": type_,
+ }
+ )
+ if name is not UNSET:
+ field_dict["name"] = name
+ if version_creation_timeout is not UNSET:
+ field_dict["versionCreationTimeout"] = version_creation_timeout
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ type_ = ManagementProjectType(d.pop("type"))
+
+ name = d.pop("name", UNSET)
+
+ def _parse_version_creation_timeout(data: object) -> bool | int | Unset:
+ if isinstance(data, Unset):
+ return data
+ return cast(bool | int | Unset, data)
+
+ version_creation_timeout = _parse_version_creation_timeout(d.pop("versionCreationTimeout", UNSET))
+
+ create_management_project_request_body = cls(
+ type_=type_,
+ name=name,
+ version_creation_timeout=version_creation_timeout,
+ )
+
+ create_management_project_request_body.additional_properties = d
+ return create_management_project_request_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/create_management_webhook_request_body.py b/packages/liveblocks-python/liveblocks/models/create_management_webhook_request_body.py
new file mode 100644
index 0000000000..83334f6458
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_management_webhook_request_body.py
@@ -0,0 +1,134 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..models.management_webhook_event import ManagementWebhookEvent
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.create_management_webhook_request_body_additional_headers import (
+ CreateManagementWebhookRequestBodyAdditionalHeaders,
+ )
+
+
+@_attrs_define
+class CreateManagementWebhookRequestBody:
+ """
+ Example:
+ {'url': 'https://example.com/webhooks', 'subscribedEvents': ['storageUpdated', 'userEntered'], 'rateLimit': 100,
+ 'storageUpdatedThrottleSeconds': 10, 'yDocUpdatedThrottleSeconds': 10}
+
+ Attributes:
+ url (str):
+ subscribed_events (list[ManagementWebhookEvent]):
+ rate_limit (int | Unset):
+ additional_headers (CreateManagementWebhookRequestBodyAdditionalHeaders | Unset):
+ storage_updated_throttle_seconds (int | Unset):
+ y_doc_updated_throttle_seconds (int | Unset):
+ """
+
+ url: str
+ subscribed_events: list[ManagementWebhookEvent]
+ rate_limit: int | Unset = UNSET
+ additional_headers: CreateManagementWebhookRequestBodyAdditionalHeaders | Unset = UNSET
+ storage_updated_throttle_seconds: int | Unset = UNSET
+ y_doc_updated_throttle_seconds: int | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ url = self.url
+
+ subscribed_events = []
+ for subscribed_events_item_data in self.subscribed_events:
+ subscribed_events_item = subscribed_events_item_data.value
+ subscribed_events.append(subscribed_events_item)
+
+ rate_limit = self.rate_limit
+
+ additional_headers: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.additional_headers, Unset):
+ additional_headers = self.additional_headers.to_dict()
+
+ storage_updated_throttle_seconds = self.storage_updated_throttle_seconds
+
+ y_doc_updated_throttle_seconds = self.y_doc_updated_throttle_seconds
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "url": url,
+ "subscribedEvents": subscribed_events,
+ }
+ )
+ if rate_limit is not UNSET:
+ field_dict["rateLimit"] = rate_limit
+ if additional_headers is not UNSET:
+ field_dict["additionalHeaders"] = additional_headers
+ if storage_updated_throttle_seconds is not UNSET:
+ field_dict["storageUpdatedThrottleSeconds"] = storage_updated_throttle_seconds
+ if y_doc_updated_throttle_seconds is not UNSET:
+ field_dict["yDocUpdatedThrottleSeconds"] = y_doc_updated_throttle_seconds
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.create_management_webhook_request_body_additional_headers import (
+ CreateManagementWebhookRequestBodyAdditionalHeaders,
+ )
+
+ d = dict(src_dict)
+ url = d.pop("url")
+
+ subscribed_events = []
+ _subscribed_events = d.pop("subscribedEvents")
+ for subscribed_events_item_data in _subscribed_events:
+ subscribed_events_item = ManagementWebhookEvent(subscribed_events_item_data)
+
+ subscribed_events.append(subscribed_events_item)
+
+ rate_limit = d.pop("rateLimit", UNSET)
+
+ _additional_headers = d.pop("additionalHeaders", UNSET)
+ additional_headers: CreateManagementWebhookRequestBodyAdditionalHeaders | Unset
+ if isinstance(_additional_headers, Unset):
+ additional_headers = UNSET
+ else:
+ additional_headers = CreateManagementWebhookRequestBodyAdditionalHeaders.from_dict(_additional_headers)
+
+ storage_updated_throttle_seconds = d.pop("storageUpdatedThrottleSeconds", UNSET)
+
+ y_doc_updated_throttle_seconds = d.pop("yDocUpdatedThrottleSeconds", UNSET)
+
+ create_management_webhook_request_body = cls(
+ url=url,
+ subscribed_events=subscribed_events,
+ rate_limit=rate_limit,
+ additional_headers=additional_headers,
+ storage_updated_throttle_seconds=storage_updated_throttle_seconds,
+ y_doc_updated_throttle_seconds=y_doc_updated_throttle_seconds,
+ )
+
+ create_management_webhook_request_body.additional_properties = d
+ return create_management_webhook_request_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/create_management_webhook_request_body_additional_headers.py b/packages/liveblocks-python/liveblocks/models/create_management_webhook_request_body_additional_headers.py
new file mode 100644
index 0000000000..26ca65b340
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_management_webhook_request_body_additional_headers.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class CreateManagementWebhookRequestBodyAdditionalHeaders:
+ """ """
+
+ additional_properties: dict[str, str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ create_management_webhook_request_body_additional_headers = cls()
+
+ create_management_webhook_request_body_additional_headers.additional_properties = d
+ return create_management_webhook_request_body_additional_headers
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/create_room_request_body.py b/packages/liveblocks-python/liveblocks/models/create_room_request_body.py
new file mode 100644
index 0000000000..6bf9932056
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_room_request_body.py
@@ -0,0 +1,148 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+from ..models.create_room_request_body_engine import CreateRoomRequestBodyEngine
+from ..models.room_permission_item import RoomPermissionItem
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.room_accesses import RoomAccesses
+ from ..models.room_metadata import RoomMetadata
+
+
+@_attrs_define
+class CreateRoomRequestBody:
+ """
+ Example:
+ {'id': 'my-room-id', 'defaultAccesses': ['room:write'], 'metadata': {'color': 'blue'}, 'usersAccesses':
+ {'alice': ['room:write']}, 'groupsAccesses': {'product': ['room:write']}}
+
+ Attributes:
+ id (str):
+ default_accesses (list[RoomPermissionItem]): Example: ['room:read', 'room:presence:write'].
+ organization_id (str | Unset):
+ users_accesses (RoomAccesses | Unset): Example: {'alice': ['room:write'], 'bob': ['room:read',
+ 'room:presence:write']}.
+ groups_accesses (RoomAccesses | Unset): Example: {'alice': ['room:write'], 'bob': ['room:read',
+ 'room:presence:write']}.
+ metadata (RoomMetadata | Unset): Example: {'color': 'blue', 'type': 'whiteboard'}.
+ engine (CreateRoomRequestBodyEngine | Unset): Preferred storage engine version to use when creating new rooms.
+ The v2 Storage engine supports larger documents, is more performant, has native streaming support, and will
+ become the default in the future.
+ """
+
+ id: str
+ default_accesses: list[RoomPermissionItem]
+ organization_id: str | Unset = UNSET
+ users_accesses: RoomAccesses | Unset = UNSET
+ groups_accesses: RoomAccesses | Unset = UNSET
+ metadata: RoomMetadata | Unset = UNSET
+ engine: CreateRoomRequestBodyEngine | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ default_accesses = []
+ for componentsschemas_room_permission_item_data in self.default_accesses:
+ componentsschemas_room_permission_item = componentsschemas_room_permission_item_data.value
+ default_accesses.append(componentsschemas_room_permission_item)
+
+ organization_id = self.organization_id
+
+ users_accesses: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.users_accesses, Unset):
+ users_accesses = self.users_accesses.to_dict()
+
+ groups_accesses: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.groups_accesses, Unset):
+ groups_accesses = self.groups_accesses.to_dict()
+
+ metadata: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.metadata, Unset):
+ metadata = self.metadata.to_dict()
+
+ engine: int | Unset = UNSET
+ if not isinstance(self.engine, Unset):
+ engine = self.engine.value
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ "defaultAccesses": default_accesses,
+ }
+ )
+ if organization_id is not UNSET:
+ field_dict["organizationId"] = organization_id
+ if users_accesses is not UNSET:
+ field_dict["usersAccesses"] = users_accesses
+ if groups_accesses is not UNSET:
+ field_dict["groupsAccesses"] = groups_accesses
+ if metadata is not UNSET:
+ field_dict["metadata"] = metadata
+ if engine is not UNSET:
+ field_dict["engine"] = engine
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.room_accesses import RoomAccesses
+ from ..models.room_metadata import RoomMetadata
+
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ default_accesses = []
+ _default_accesses = d.pop("defaultAccesses")
+ for componentsschemas_room_permission_item_data in _default_accesses:
+ componentsschemas_room_permission_item = RoomPermissionItem(componentsschemas_room_permission_item_data)
+
+ default_accesses.append(componentsschemas_room_permission_item)
+
+ organization_id = d.pop("organizationId", UNSET)
+
+ _users_accesses = d.pop("usersAccesses", UNSET)
+ users_accesses: RoomAccesses | Unset
+ if isinstance(_users_accesses, Unset):
+ users_accesses = UNSET
+ else:
+ users_accesses = RoomAccesses.from_dict(_users_accesses)
+
+ _groups_accesses = d.pop("groupsAccesses", UNSET)
+ groups_accesses: RoomAccesses | Unset
+ if isinstance(_groups_accesses, Unset):
+ groups_accesses = UNSET
+ else:
+ groups_accesses = RoomAccesses.from_dict(_groups_accesses)
+
+ _metadata = d.pop("metadata", UNSET)
+ metadata: RoomMetadata | Unset
+ if isinstance(_metadata, Unset):
+ metadata = UNSET
+ else:
+ metadata = RoomMetadata.from_dict(_metadata)
+
+ _engine = d.pop("engine", UNSET)
+ engine: CreateRoomRequestBodyEngine | Unset
+ if isinstance(_engine, Unset):
+ engine = UNSET
+ else:
+ engine = CreateRoomRequestBodyEngine(_engine)
+
+ create_room_request_body = cls(
+ id=id,
+ default_accesses=default_accesses,
+ organization_id=organization_id,
+ users_accesses=users_accesses,
+ groups_accesses=groups_accesses,
+ metadata=metadata,
+ engine=engine,
+ )
+
+ return create_room_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/create_room_request_body_engine.py b/packages/liveblocks-python/liveblocks/models/create_room_request_body_engine.py
new file mode 100644
index 0000000000..9214523d2a
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_room_request_body_engine.py
@@ -0,0 +1,9 @@
+from enum import IntEnum
+
+
+class CreateRoomRequestBodyEngine(IntEnum):
+ VALUE_1 = 1
+ VALUE_2 = 2
+
+ def __str__(self) -> str:
+ return str(self.value)
diff --git a/packages/liveblocks-python/liveblocks/models/create_thread_request_body.py b/packages/liveblocks-python/liveblocks/models/create_thread_request_body.py
new file mode 100644
index 0000000000..ff8cfefa9f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_thread_request_body.py
@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.create_thread_request_body_comment import CreateThreadRequestBodyComment
+ from ..models.thread_metadata import ThreadMetadata
+
+
+@_attrs_define
+class CreateThreadRequestBody:
+ """
+ Attributes:
+ comment (CreateThreadRequestBodyComment):
+ metadata (ThreadMetadata | Unset): Custom metadata attached to a thread. Supports maximum 50 entries. Key length
+ has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+ """
+
+ comment: CreateThreadRequestBodyComment
+ metadata: ThreadMetadata | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ comment = self.comment.to_dict()
+
+ metadata: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.metadata, Unset):
+ metadata = self.metadata.to_dict()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "comment": comment,
+ }
+ )
+ if metadata is not UNSET:
+ field_dict["metadata"] = metadata
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.create_thread_request_body_comment import CreateThreadRequestBodyComment
+ from ..models.thread_metadata import ThreadMetadata
+
+ d = dict(src_dict)
+ comment = CreateThreadRequestBodyComment.from_dict(d.pop("comment"))
+
+ _metadata = d.pop("metadata", UNSET)
+ metadata: ThreadMetadata | Unset
+ if isinstance(_metadata, Unset):
+ metadata = UNSET
+ else:
+ metadata = ThreadMetadata.from_dict(_metadata)
+
+ create_thread_request_body = cls(
+ comment=comment,
+ metadata=metadata,
+ )
+
+ create_thread_request_body.additional_properties = d
+ return create_thread_request_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/create_thread_request_body_comment.py b/packages/liveblocks-python/liveblocks/models/create_thread_request_body_comment.py
new file mode 100644
index 0000000000..8a8bf78969
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_thread_request_body_comment.py
@@ -0,0 +1,93 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.comment_body import CommentBody
+ from ..models.comment_metadata import CommentMetadata
+
+
+@_attrs_define
+class CreateThreadRequestBodyComment:
+ """
+ Attributes:
+ user_id (str):
+ body (CommentBody): Example: {'version': 1, 'content': [{'type': 'paragraph', 'children': [{'text': 'Hello '},
+ {'text': 'world', 'bold': True}]}]}.
+ created_at (datetime.datetime | Unset):
+ metadata (CommentMetadata | Unset): Custom metadata attached to a comment. Supports maximum 50 entries. Key
+ length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+ """
+
+ user_id: str
+ body: CommentBody
+ created_at: datetime.datetime | Unset = UNSET
+ metadata: CommentMetadata | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ body = self.body.to_dict()
+
+ created_at: str | Unset = UNSET
+ if not isinstance(self.created_at, Unset):
+ created_at = self.created_at.isoformat()
+
+ metadata: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.metadata, Unset):
+ metadata = self.metadata.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ "body": body,
+ }
+ )
+ if created_at is not UNSET:
+ field_dict["createdAt"] = created_at
+ if metadata is not UNSET:
+ field_dict["metadata"] = metadata
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.comment_body import CommentBody
+ from ..models.comment_metadata import CommentMetadata
+
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ body = CommentBody.from_dict(d.pop("body"))
+
+ _created_at = d.pop("createdAt", UNSET)
+ created_at: datetime.datetime | Unset
+ if isinstance(_created_at, Unset):
+ created_at = UNSET
+ else:
+ created_at = isoparse(_created_at)
+
+ _metadata = d.pop("metadata", UNSET)
+ metadata: CommentMetadata | Unset
+ if isinstance(_metadata, Unset):
+ metadata = UNSET
+ else:
+ metadata = CommentMetadata.from_dict(_metadata)
+
+ create_thread_request_body_comment = cls(
+ user_id=user_id,
+ body=body,
+ created_at=created_at,
+ metadata=metadata,
+ )
+
+ return create_thread_request_body_comment
diff --git a/packages/liveblocks-python/liveblocks/models/create_web_knowledge_source_request_body.py b/packages/liveblocks-python/liveblocks/models/create_web_knowledge_source_request_body.py
new file mode 100644
index 0000000000..bfa11ee483
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_web_knowledge_source_request_body.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..models.create_web_knowledge_source_request_body_type import CreateWebKnowledgeSourceRequestBodyType
+
+
+@_attrs_define
+class CreateWebKnowledgeSourceRequestBody:
+ """
+ Example:
+ {'copilotId': 'cp_abc123', 'url': 'https://docs.example.com', 'type': 'crawl'}
+
+ Attributes:
+ copilot_id (str):
+ url (str):
+ type_ (CreateWebKnowledgeSourceRequestBodyType):
+ """
+
+ copilot_id: str
+ url: str
+ type_: CreateWebKnowledgeSourceRequestBodyType
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ copilot_id = self.copilot_id
+
+ url = self.url
+
+ type_ = self.type_.value
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "copilotId": copilot_id,
+ "url": url,
+ "type": type_,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ copilot_id = d.pop("copilotId")
+
+ url = d.pop("url")
+
+ type_ = CreateWebKnowledgeSourceRequestBodyType(d.pop("type"))
+
+ create_web_knowledge_source_request_body = cls(
+ copilot_id=copilot_id,
+ url=url,
+ type_=type_,
+ )
+
+ create_web_knowledge_source_request_body.additional_properties = d
+ return create_web_knowledge_source_request_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/create_web_knowledge_source_request_body_type.py b/packages/liveblocks-python/liveblocks/models/create_web_knowledge_source_request_body_type.py
new file mode 100644
index 0000000000..3c997b2e70
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_web_knowledge_source_request_body_type.py
@@ -0,0 +1,7 @@
+from enum import StrEnum
+
+
+class CreateWebKnowledgeSourceRequestBodyType(StrEnum):
+ CRAWL = "crawl"
+ INDIVIDUAL_LINK = "individual_link"
+ SITEMAP = "sitemap"
diff --git a/packages/liveblocks-python/liveblocks/models/create_web_knowledge_source_response.py b/packages/liveblocks-python/liveblocks/models/create_web_knowledge_source_response.py
new file mode 100644
index 0000000000..baba6df6dd
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_web_knowledge_source_response.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class CreateWebKnowledgeSourceResponse:
+ """
+ Example:
+ {'id': 'ks_abc123'}
+
+ Attributes:
+ id (str):
+ """
+
+ id: str
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ create_web_knowledge_source_response = cls(
+ id=id,
+ )
+
+ return create_web_knowledge_source_response
diff --git a/packages/liveblocks-python/liveblocks/models/create_yjs_version_response.py b/packages/liveblocks-python/liveblocks/models/create_yjs_version_response.py
new file mode 100644
index 0000000000..03f2177f89
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_yjs_version_response.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.create_yjs_version_response_data import CreateYjsVersionResponseData
+
+
+@_attrs_define
+class CreateYjsVersionResponse:
+ """
+ Example:
+ {'data': {'id': 'vh_abc123'}}
+
+ Attributes:
+ data (CreateYjsVersionResponseData):
+ """
+
+ data: CreateYjsVersionResponseData
+
+ def to_dict(self) -> dict[str, Any]:
+ data = self.data.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.create_yjs_version_response_data import CreateYjsVersionResponseData
+
+ d = dict(src_dict)
+ data = CreateYjsVersionResponseData.from_dict(d.pop("data"))
+
+ create_yjs_version_response = cls(
+ data=data,
+ )
+
+ return create_yjs_version_response
diff --git a/packages/liveblocks-python/liveblocks/models/create_yjs_version_response_data.py b/packages/liveblocks-python/liveblocks/models/create_yjs_version_response_data.py
new file mode 100644
index 0000000000..f958c26db0
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_yjs_version_response_data.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class CreateYjsVersionResponseData:
+ """
+ Attributes:
+ id (str): Unique identifier for the created version
+ """
+
+ id: str
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ create_yjs_version_response_data = cls(
+ id=id,
+ )
+
+ return create_yjs_version_response_data
diff --git a/packages/liveblocks-python/liveblocks/models/delete_management_webhook_headers_request_body.py b/packages/liveblocks-python/liveblocks/models/delete_management_webhook_headers_request_body.py
new file mode 100644
index 0000000000..8e05eb2f0f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/delete_management_webhook_headers_request_body.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class DeleteManagementWebhookHeadersRequestBody:
+ """
+ Example:
+ {'headers': ['X-Custom-Header', 'X-Another-Header']}
+
+ Attributes:
+ headers (list[str]):
+ """
+
+ headers: list[str]
+
+ def to_dict(self) -> dict[str, Any]:
+ headers = self.headers
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "headers": headers,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ headers = cast(list[str], d.pop("headers"))
+
+ delete_management_webhook_headers_request_body = cls(
+ headers=headers,
+ )
+
+ return delete_management_webhook_headers_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/delete_management_webhook_headers_response.py b/packages/liveblocks-python/liveblocks/models/delete_management_webhook_headers_response.py
new file mode 100644
index 0000000000..a317cc7669
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/delete_management_webhook_headers_response.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.management_webhook_additional_headers import ManagementWebhookAdditionalHeaders
+
+
+@_attrs_define
+class DeleteManagementWebhookHeadersResponse:
+ """
+ Example:
+ {'headers': {'X-Remaining-Header': 'value'}}
+
+ Attributes:
+ headers (ManagementWebhookAdditionalHeaders): Example: {'X-Custom-Header': 'value'}.
+ """
+
+ headers: ManagementWebhookAdditionalHeaders
+
+ def to_dict(self) -> dict[str, Any]:
+ headers = self.headers.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "headers": headers,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.management_webhook_additional_headers import ManagementWebhookAdditionalHeaders
+
+ d = dict(src_dict)
+ headers = ManagementWebhookAdditionalHeaders.from_dict(d.pop("headers"))
+
+ delete_management_webhook_headers_response = cls(
+ headers=headers,
+ )
+
+ return delete_management_webhook_headers_response
diff --git a/packages/liveblocks-python/liveblocks/models/edit_comment_metadata_request_body.py b/packages/liveblocks-python/liveblocks/models/edit_comment_metadata_request_body.py
new file mode 100644
index 0000000000..7653ee47ca
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/edit_comment_metadata_request_body.py
@@ -0,0 +1,73 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.edit_comment_metadata_request_body_metadata import EditCommentMetadataRequestBodyMetadata
+
+
+@_attrs_define
+class EditCommentMetadataRequestBody:
+ """
+ Attributes:
+ metadata (EditCommentMetadataRequestBodyMetadata):
+ user_id (str):
+ updated_at (datetime.datetime | Unset):
+ """
+
+ metadata: EditCommentMetadataRequestBodyMetadata
+ user_id: str
+ updated_at: datetime.datetime | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ metadata = self.metadata.to_dict()
+
+ user_id = self.user_id
+
+ updated_at: str | Unset = UNSET
+ if not isinstance(self.updated_at, Unset):
+ updated_at = self.updated_at.isoformat()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "metadata": metadata,
+ "userId": user_id,
+ }
+ )
+ if updated_at is not UNSET:
+ field_dict["updatedAt"] = updated_at
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.edit_comment_metadata_request_body_metadata import EditCommentMetadataRequestBodyMetadata
+
+ d = dict(src_dict)
+ metadata = EditCommentMetadataRequestBodyMetadata.from_dict(d.pop("metadata"))
+
+ user_id = d.pop("userId")
+
+ _updated_at = d.pop("updatedAt", UNSET)
+ updated_at: datetime.datetime | Unset
+ if isinstance(_updated_at, Unset):
+ updated_at = UNSET
+ else:
+ updated_at = isoparse(_updated_at)
+
+ edit_comment_metadata_request_body = cls(
+ metadata=metadata,
+ user_id=user_id,
+ updated_at=updated_at,
+ )
+
+ return edit_comment_metadata_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/edit_comment_metadata_request_body_metadata.py b/packages/liveblocks-python/liveblocks/models/edit_comment_metadata_request_body_metadata.py
new file mode 100644
index 0000000000..10b49e8221
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/edit_comment_metadata_request_body_metadata.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class EditCommentMetadataRequestBodyMetadata:
+ """ """
+
+ additional_properties: dict[str, bool | float | None | str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ field_dict[prop_name] = prop
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ edit_comment_metadata_request_body_metadata = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+
+ def _parse_additional_property(data: object) -> bool | float | None | str:
+ if data is None:
+ return data
+ return cast(bool | float | None | str, data)
+
+ additional_property = _parse_additional_property(prop_dict)
+
+ additional_properties[prop_name] = additional_property
+
+ edit_comment_metadata_request_body_metadata.additional_properties = additional_properties
+ return edit_comment_metadata_request_body_metadata
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> bool | float | None | str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: bool | float | None | str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/edit_comment_request_body.py b/packages/liveblocks-python/liveblocks/models/edit_comment_request_body.py
new file mode 100644
index 0000000000..75ee9aca11
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/edit_comment_request_body.py
@@ -0,0 +1,96 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.comment_body import CommentBody
+ from ..models.comment_metadata import CommentMetadata
+
+
+@_attrs_define
+class EditCommentRequestBody:
+ """
+ Attributes:
+ body (CommentBody): Example: {'version': 1, 'content': [{'type': 'paragraph', 'children': [{'text': 'Hello '},
+ {'text': 'world', 'bold': True}]}]}.
+ edited_at (datetime.datetime | Unset):
+ metadata (CommentMetadata | Unset): Custom metadata attached to a comment. Supports maximum 50 entries. Key
+ length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+ attachment_ids (list[str] | Unset):
+ """
+
+ body: CommentBody
+ edited_at: datetime.datetime | Unset = UNSET
+ metadata: CommentMetadata | Unset = UNSET
+ attachment_ids: list[str] | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ body = self.body.to_dict()
+
+ edited_at: str | Unset = UNSET
+ if not isinstance(self.edited_at, Unset):
+ edited_at = self.edited_at.isoformat()
+
+ metadata: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.metadata, Unset):
+ metadata = self.metadata.to_dict()
+
+ attachment_ids: list[str] | Unset = UNSET
+ if not isinstance(self.attachment_ids, Unset):
+ attachment_ids = self.attachment_ids
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "body": body,
+ }
+ )
+ if edited_at is not UNSET:
+ field_dict["editedAt"] = edited_at
+ if metadata is not UNSET:
+ field_dict["metadata"] = metadata
+ if attachment_ids is not UNSET:
+ field_dict["attachmentIds"] = attachment_ids
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.comment_body import CommentBody
+ from ..models.comment_metadata import CommentMetadata
+
+ d = dict(src_dict)
+ body = CommentBody.from_dict(d.pop("body"))
+
+ _edited_at = d.pop("editedAt", UNSET)
+ edited_at: datetime.datetime | Unset
+ if isinstance(_edited_at, Unset):
+ edited_at = UNSET
+ else:
+ edited_at = isoparse(_edited_at)
+
+ _metadata = d.pop("metadata", UNSET)
+ metadata: CommentMetadata | Unset
+ if isinstance(_metadata, Unset):
+ metadata = UNSET
+ else:
+ metadata = CommentMetadata.from_dict(_metadata)
+
+ attachment_ids = cast(list[str], d.pop("attachmentIds", UNSET))
+
+ edit_comment_request_body = cls(
+ body=body,
+ edited_at=edited_at,
+ metadata=metadata,
+ attachment_ids=attachment_ids,
+ )
+
+ return edit_comment_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/edit_thread_metadata_request_body.py b/packages/liveblocks-python/liveblocks/models/edit_thread_metadata_request_body.py
new file mode 100644
index 0000000000..84f2891589
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/edit_thread_metadata_request_body.py
@@ -0,0 +1,92 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.edit_thread_metadata_request_body_metadata import EditThreadMetadataRequestBodyMetadata
+
+
+@_attrs_define
+class EditThreadMetadataRequestBody:
+ """
+ Attributes:
+ metadata (EditThreadMetadataRequestBodyMetadata):
+ user_id (str):
+ updated_at (datetime.datetime | Unset):
+ """
+
+ metadata: EditThreadMetadataRequestBodyMetadata
+ user_id: str
+ updated_at: datetime.datetime | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ metadata = self.metadata.to_dict()
+
+ user_id = self.user_id
+
+ updated_at: str | Unset = UNSET
+ if not isinstance(self.updated_at, Unset):
+ updated_at = self.updated_at.isoformat()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "metadata": metadata,
+ "userId": user_id,
+ }
+ )
+ if updated_at is not UNSET:
+ field_dict["updatedAt"] = updated_at
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.edit_thread_metadata_request_body_metadata import EditThreadMetadataRequestBodyMetadata
+
+ d = dict(src_dict)
+ metadata = EditThreadMetadataRequestBodyMetadata.from_dict(d.pop("metadata"))
+
+ user_id = d.pop("userId")
+
+ _updated_at = d.pop("updatedAt", UNSET)
+ updated_at: datetime.datetime | Unset
+ if isinstance(_updated_at, Unset):
+ updated_at = UNSET
+ else:
+ updated_at = isoparse(_updated_at)
+
+ edit_thread_metadata_request_body = cls(
+ metadata=metadata,
+ user_id=user_id,
+ updated_at=updated_at,
+ )
+
+ edit_thread_metadata_request_body.additional_properties = d
+ return edit_thread_metadata_request_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/edit_thread_metadata_request_body_metadata.py b/packages/liveblocks-python/liveblocks/models/edit_thread_metadata_request_body_metadata.py
new file mode 100644
index 0000000000..d94a8e1969
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/edit_thread_metadata_request_body_metadata.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class EditThreadMetadataRequestBodyMetadata:
+ """ """
+
+ additional_properties: dict[str, bool | float | None | str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ field_dict[prop_name] = prop
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ edit_thread_metadata_request_body_metadata = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+
+ def _parse_additional_property(data: object) -> bool | float | None | str:
+ if data is None:
+ return data
+ return cast(bool | float | None | str, data)
+
+ additional_property = _parse_additional_property(prop_dict)
+
+ additional_properties[prop_name] = additional_property
+
+ edit_thread_metadata_request_body_metadata.additional_properties = additional_properties
+ return edit_thread_metadata_request_body_metadata
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> bool | float | None | str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: bool | float | None | str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/error.py b/packages/liveblocks-python/liveblocks/models/error.py
new file mode 100644
index 0000000000..2bd79e0171
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/error.py
@@ -0,0 +1,86 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class Error:
+ """
+ Attributes:
+ error (str | Unset): Error code
+ message (str | Unset): Message explaining the error
+ suggestion (str | Unset): A suggestion on how to fix the error
+ docs (str | Unset): A link to the documentation
+ """
+
+ error: str | Unset = UNSET
+ message: str | Unset = UNSET
+ suggestion: str | Unset = UNSET
+ docs: str | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ error = self.error
+
+ message = self.message
+
+ suggestion = self.suggestion
+
+ docs = self.docs
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if error is not UNSET:
+ field_dict["error"] = error
+ if message is not UNSET:
+ field_dict["message"] = message
+ if suggestion is not UNSET:
+ field_dict["suggestion"] = suggestion
+ if docs is not UNSET:
+ field_dict["docs"] = docs
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ error = d.pop("error", UNSET)
+
+ message = d.pop("message", UNSET)
+
+ suggestion = d.pop("suggestion", UNSET)
+
+ docs = d.pop("docs", UNSET)
+
+ error = cls(
+ error=error,
+ message=message,
+ suggestion=suggestion,
+ docs=docs,
+ )
+
+ error.additional_properties = d
+ return error
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/get_ai_copilots_response.py b/packages/liveblocks-python/liveblocks/models/get_ai_copilots_response.py
new file mode 100644
index 0000000000..df1eaeaabd
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_ai_copilots_response.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_anthropic import AiCopilotAnthropic
+ from ..models.ai_copilot_google import AiCopilotGoogle
+ from ..models.ai_copilot_open_ai import AiCopilotOpenAi
+ from ..models.ai_copilot_open_ai_compatible import AiCopilotOpenAiCompatible
+
+
+@_attrs_define
+class GetAiCopilotsResponse:
+ """
+ Example:
+ {'nextCursor': None, 'data': [{'type': 'copilot', 'id': 'cp_abc123', 'name': 'My Copilot', 'systemPrompt': 'You
+ are a helpful assistant.', 'alwaysUseKnowledge': True, 'createdAt': '2024-06-01T12:00:00.000Z', 'updatedAt':
+ '2024-06-01T12:00:00.000Z', 'provider': 'openai', 'providerModel': 'gpt-4o'}]}
+
+ Attributes:
+ next_cursor (None | str):
+ data (list[AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible]):
+ """
+
+ next_cursor: None | str
+ data: list[AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible]
+
+ def to_dict(self) -> dict[str, Any]:
+ from ..models.ai_copilot_anthropic import AiCopilotAnthropic
+ from ..models.ai_copilot_google import AiCopilotGoogle
+ from ..models.ai_copilot_open_ai import AiCopilotOpenAi
+
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ data = []
+ for data_item_data in self.data:
+ data_item: dict[str, Any]
+ if isinstance(data_item_data, AiCopilotOpenAi):
+ data_item = data_item_data.to_dict()
+ elif isinstance(data_item_data, AiCopilotAnthropic):
+ data_item = data_item_data.to_dict()
+ elif isinstance(data_item_data, AiCopilotGoogle):
+ data_item = data_item_data.to_dict()
+ else:
+ data_item = data_item_data.to_dict()
+
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "nextCursor": next_cursor,
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_anthropic import AiCopilotAnthropic
+ from ..models.ai_copilot_google import AiCopilotGoogle
+ from ..models.ai_copilot_open_ai import AiCopilotOpenAi
+ from ..models.ai_copilot_open_ai_compatible import AiCopilotOpenAiCompatible
+
+ d = dict(src_dict)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+
+ def _parse_data_item(
+ data: object,
+ ) -> AiCopilotAnthropic | AiCopilotGoogle | AiCopilotOpenAi | AiCopilotOpenAiCompatible:
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_0 = AiCopilotOpenAi.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_1 = AiCopilotAnthropic.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_1
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_2 = AiCopilotGoogle.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_2
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_ai_copilot_type_3 = AiCopilotOpenAiCompatible.from_dict(data)
+
+ return componentsschemas_ai_copilot_type_3
+
+ data_item = _parse_data_item(data_item_data)
+
+ data.append(data_item)
+
+ get_ai_copilots_response = cls(
+ next_cursor=next_cursor,
+ data=data,
+ )
+
+ return get_ai_copilots_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_file_knowledge_source_markdown_response.py b/packages/liveblocks-python/liveblocks/models/get_file_knowledge_source_markdown_response.py
new file mode 100644
index 0000000000..5a8f597dee
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_file_knowledge_source_markdown_response.py
@@ -0,0 +1,51 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class GetFileKnowledgeSourceMarkdownResponse:
+ r"""
+ Example:
+ {'id': 'ks_abc123', 'content': '# Document Title\n\nThis is the content of the uploaded file.'}
+
+ Attributes:
+ id (str):
+ content (str):
+ """
+
+ id: str
+ content: str
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ content = self.content
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ "content": content,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ content = d.pop("content")
+
+ get_file_knowledge_source_markdown_response = cls(
+ id=id,
+ content=content,
+ )
+
+ return get_file_knowledge_source_markdown_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_groups_response.py b/packages/liveblocks-python/liveblocks/models/get_groups_response.py
new file mode 100644
index 0000000000..a74cd93dd2
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_groups_response.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.group import Group
+
+
+@_attrs_define
+class GetGroupsResponse:
+ """
+ Example:
+ {'data': [{'type': 'group', 'id': 'engineering', 'organizationId': 'org_123456789', 'createdAt':
+ '2024-01-15T10:30:00.000Z', 'updatedAt': '2024-01-15T10:30:00.000Z', 'scopes': {'mention': True}, 'members':
+ [{'id': 'alice', 'addedAt': '2024-01-15T10:30:00.000Z'}]}], 'nextCursor': None}
+
+ Attributes:
+ data (list[Group]):
+ next_cursor (None | str):
+ """
+
+ data: list[Group]
+ next_cursor: None | str
+
+ def to_dict(self) -> dict[str, Any]:
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "data": data,
+ "nextCursor": next_cursor,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.group import Group
+
+ d = dict(src_dict)
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+ data_item = Group.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ get_groups_response = cls(
+ data=data,
+ next_cursor=next_cursor,
+ )
+
+ return get_groups_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_inbox_notifications_response.py b/packages/liveblocks-python/liveblocks/models/get_inbox_notifications_response.py
new file mode 100644
index 0000000000..b40b399c65
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_inbox_notifications_response.py
@@ -0,0 +1,98 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.inbox_notification_custom_data import InboxNotificationCustomData
+ from ..models.inbox_notification_thread_data import InboxNotificationThreadData
+
+
+@_attrs_define
+class GetInboxNotificationsResponse:
+ """
+ Example:
+ {'nextCursor': None, 'data': [{'kind': 'thread', 'id': 'in_abc123', 'roomId': 'my-room-id', 'threadId':
+ 'th_abc123', 'notifiedAt': '2024-01-15T10:30:00.000Z', 'readAt': None}]}
+
+ Attributes:
+ next_cursor (None | str): A cursor to use for pagination. Pass this value as `startingAfter` to get the next
+ page of results. `null` if there are no more results.
+ data (list[InboxNotificationCustomData | InboxNotificationThreadData]):
+ """
+
+ next_cursor: None | str
+ data: list[InboxNotificationCustomData | InboxNotificationThreadData]
+
+ def to_dict(self) -> dict[str, Any]:
+ from ..models.inbox_notification_thread_data import InboxNotificationThreadData
+
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ data = []
+ for data_item_data in self.data:
+ data_item: dict[str, Any]
+ if isinstance(data_item_data, InboxNotificationThreadData):
+ data_item = data_item_data.to_dict()
+ else:
+ data_item = data_item_data.to_dict()
+
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "nextCursor": next_cursor,
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.inbox_notification_custom_data import InboxNotificationCustomData
+ from ..models.inbox_notification_thread_data import InboxNotificationThreadData
+
+ d = dict(src_dict)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+
+ def _parse_data_item(data: object) -> InboxNotificationCustomData | InboxNotificationThreadData:
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ data_item_type_0 = InboxNotificationThreadData.from_dict(data)
+
+ return data_item_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ if not isinstance(data, dict):
+ raise TypeError()
+ data_item_type_1 = InboxNotificationCustomData.from_dict(data)
+
+ return data_item_type_1
+
+ data_item = _parse_data_item(data_item_data)
+
+ data.append(data_item)
+
+ get_inbox_notifications_response = cls(
+ next_cursor=next_cursor,
+ data=data,
+ )
+
+ return get_inbox_notifications_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_knowledge_sources_response.py b/packages/liveblocks-python/liveblocks/models/get_knowledge_sources_response.py
new file mode 100644
index 0000000000..5cb74a1ca7
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_knowledge_sources_response.py
@@ -0,0 +1,98 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.knowledge_source_file_source import KnowledgeSourceFileSource
+ from ..models.knowledge_source_web_source import KnowledgeSourceWebSource
+
+
+@_attrs_define
+class GetKnowledgeSourcesResponse:
+ """
+ Example:
+ {'nextCursor': None, 'data': [{'id': 'ks_abc123', 'type': 'ai-knowledge-web-source', 'createdAt':
+ '2024-06-01T12:00:00.000Z', 'updatedAt': '2024-06-01T12:00:00.000Z', 'lastIndexedAt':
+ '2024-06-01T12:00:00.000Z', 'status': 'ready', 'link': {'url': 'https://docs.example.com', 'type': 'crawl'}}]}
+
+ Attributes:
+ next_cursor (None | str):
+ data (list[KnowledgeSourceFileSource | KnowledgeSourceWebSource]):
+ """
+
+ next_cursor: None | str
+ data: list[KnowledgeSourceFileSource | KnowledgeSourceWebSource]
+
+ def to_dict(self) -> dict[str, Any]:
+ from ..models.knowledge_source_web_source import KnowledgeSourceWebSource
+
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ data = []
+ for data_item_data in self.data:
+ data_item: dict[str, Any]
+ if isinstance(data_item_data, KnowledgeSourceWebSource):
+ data_item = data_item_data.to_dict()
+ else:
+ data_item = data_item_data.to_dict()
+
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "nextCursor": next_cursor,
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.knowledge_source_file_source import KnowledgeSourceFileSource
+ from ..models.knowledge_source_web_source import KnowledgeSourceWebSource
+
+ d = dict(src_dict)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+
+ def _parse_data_item(data: object) -> KnowledgeSourceFileSource | KnowledgeSourceWebSource:
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_knowledge_source_type_0 = KnowledgeSourceWebSource.from_dict(data)
+
+ return componentsschemas_knowledge_source_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ if not isinstance(data, dict):
+ raise TypeError()
+ componentsschemas_knowledge_source_type_1 = KnowledgeSourceFileSource.from_dict(data)
+
+ return componentsschemas_knowledge_source_type_1
+
+ data_item = _parse_data_item(data_item_data)
+
+ data.append(data_item)
+
+ get_knowledge_sources_response = cls(
+ next_cursor=next_cursor,
+ data=data,
+ )
+
+ return get_knowledge_sources_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_management_projects_response.py b/packages/liveblocks-python/liveblocks/models/get_management_projects_response.py
new file mode 100644
index 0000000000..36859cdc21
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_management_projects_response.py
@@ -0,0 +1,82 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.management_project import ManagementProject
+
+
+@_attrs_define
+class GetManagementProjectsResponse:
+ """
+ Example:
+ {'data': [{'id': '683d49ed6b4d1cec5a597b13', 'teamId': 'team_123', 'type': 'dev', 'name': 'My Project',
+ 'createdAt': '2024-09-03T12:34:56.000Z', 'updatedAt': '2024-09-03T12:34:56.000Z', 'publicKey': {'activated':
+ True, 'createdAt': '2024-09-03T12:34:56.000Z', 'value': 'pk_dev_123'}, 'secretKey': {'createdAt':
+ '2024-09-03T12:34:56.000Z', 'value': 'sk_dev_123'}, 'region': 'earth', 'versionCreationTimeout': False}],
+ 'nextCursor': None}
+
+ Attributes:
+ next_cursor (None | str):
+ data (list[ManagementProject] | Unset):
+ """
+
+ next_cursor: None | str
+ data: list[ManagementProject] | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ data: list[dict[str, Any]] | Unset = UNSET
+ if not isinstance(self.data, Unset):
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "nextCursor": next_cursor,
+ }
+ )
+ if data is not UNSET:
+ field_dict["data"] = data
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.management_project import ManagementProject
+
+ d = dict(src_dict)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ _data = d.pop("data", UNSET)
+ data: list[ManagementProject] | Unset = UNSET
+ if _data is not UNSET:
+ data = []
+ for data_item_data in _data:
+ data_item = ManagementProject.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ get_management_projects_response = cls(
+ next_cursor=next_cursor,
+ data=data,
+ )
+
+ return get_management_projects_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_management_webhook_headers_response.py b/packages/liveblocks-python/liveblocks/models/get_management_webhook_headers_response.py
new file mode 100644
index 0000000000..4e8f52bfae
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_management_webhook_headers_response.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.management_webhook_additional_headers import ManagementWebhookAdditionalHeaders
+
+
+@_attrs_define
+class GetManagementWebhookHeadersResponse:
+ """
+ Example:
+ {'headers': {'X-Custom-Header': 'value'}}
+
+ Attributes:
+ headers (ManagementWebhookAdditionalHeaders): Example: {'X-Custom-Header': 'value'}.
+ """
+
+ headers: ManagementWebhookAdditionalHeaders
+
+ def to_dict(self) -> dict[str, Any]:
+ headers = self.headers.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "headers": headers,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.management_webhook_additional_headers import ManagementWebhookAdditionalHeaders
+
+ d = dict(src_dict)
+ headers = ManagementWebhookAdditionalHeaders.from_dict(d.pop("headers"))
+
+ get_management_webhook_headers_response = cls(
+ headers=headers,
+ )
+
+ return get_management_webhook_headers_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_management_webhooks_response.py b/packages/liveblocks-python/liveblocks/models/get_management_webhooks_response.py
new file mode 100644
index 0000000000..b6d981a0a8
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_management_webhooks_response.py
@@ -0,0 +1,81 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.management_webhook import ManagementWebhook
+
+
+@_attrs_define
+class GetManagementWebhooksResponse:
+ """
+ Example:
+ {'data': [{'id': 'wh_abc123', 'createdAt': '2024-09-03T12:34:56.000Z', 'updatedAt': '2024-09-03T12:34:56.000Z',
+ 'url': 'https://example.com/webhooks', 'disabled': False, 'subscribedEvents': ['storageUpdated', 'userEntered'],
+ 'secret': {'value': 'whsec_abc123'}, 'storageUpdatedThrottleSeconds': 10, 'yDocUpdatedThrottleSeconds': 10}],
+ 'nextCursor': None}
+
+ Attributes:
+ next_cursor (None | str):
+ data (list[ManagementWebhook] | Unset):
+ """
+
+ next_cursor: None | str
+ data: list[ManagementWebhook] | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ data: list[dict[str, Any]] | Unset = UNSET
+ if not isinstance(self.data, Unset):
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "nextCursor": next_cursor,
+ }
+ )
+ if data is not UNSET:
+ field_dict["data"] = data
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.management_webhook import ManagementWebhook
+
+ d = dict(src_dict)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ _data = d.pop("data", UNSET)
+ data: list[ManagementWebhook] | Unset = UNSET
+ if _data is not UNSET:
+ data = []
+ for data_item_data in _data:
+ data_item = ManagementWebhook.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ get_management_webhooks_response = cls(
+ next_cursor=next_cursor,
+ data=data,
+ )
+
+ return get_management_webhooks_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_room_subscription_settings_response.py b/packages/liveblocks-python/liveblocks/models/get_room_subscription_settings_response.py
new file mode 100644
index 0000000000..6ce5e5cebf
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_room_subscription_settings_response.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.user_room_subscription_settings import UserRoomSubscriptionSettings
+
+
+@_attrs_define
+class GetRoomSubscriptionSettingsResponse:
+ """
+ Example:
+ {'nextCursor': None, 'data': [{'threads': 'all', 'textMentions': 'mine', 'roomId': 'my-room-id'}]}
+
+ Attributes:
+ next_cursor (None | str): A cursor to use for pagination. Pass this value as `startingAfter` to get the next
+ page of results. `null` if there are no more results.
+ data (list[UserRoomSubscriptionSettings]):
+ """
+
+ next_cursor: None | str
+ data: list[UserRoomSubscriptionSettings]
+
+ def to_dict(self) -> dict[str, Any]:
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "nextCursor": next_cursor,
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.user_room_subscription_settings import UserRoomSubscriptionSettings
+
+ d = dict(src_dict)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+ data_item = UserRoomSubscriptionSettings.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ get_room_subscription_settings_response = cls(
+ next_cursor=next_cursor,
+ data=data,
+ )
+
+ return get_room_subscription_settings_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_rooms_response.py b/packages/liveblocks-python/liveblocks/models/get_rooms_response.py
new file mode 100644
index 0000000000..78bffc06d3
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_rooms_response.py
@@ -0,0 +1,74 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.room import Room
+
+
+@_attrs_define
+class GetRoomsResponse:
+ """
+ Example:
+ {'nextCursor': 'eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9', 'data': [{'type': 'room', 'id': 'my-room-id',
+ 'lastConnectionAt': '2022-08-08T23:23:15.281Z', 'createdAt': '2022-08-08T23:23:15.281Z', 'organizationId':
+ 'org_123456789', 'metadata': {'color': 'blue'}, 'defaultAccesses': ['room:write'], 'groupsAccesses': {'product':
+ ['room:write']}, 'usersAccesses': {'alice': ['room:write']}}]}
+
+ Attributes:
+ next_cursor (None | str):
+ data (list[Room]):
+ """
+
+ next_cursor: None | str
+ data: list[Room]
+
+ def to_dict(self) -> dict[str, Any]:
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "nextCursor": next_cursor,
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.room import Room
+
+ d = dict(src_dict)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+ data_item = Room.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ get_rooms_response = cls(
+ next_cursor=next_cursor,
+ data=data,
+ )
+
+ return get_rooms_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_storage_document_format.py b/packages/liveblocks-python/liveblocks/models/get_storage_document_format.py
new file mode 100644
index 0000000000..69a58eb26c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_storage_document_format.py
@@ -0,0 +1,6 @@
+from enum import StrEnum
+
+
+class GetStorageDocumentFormat(StrEnum):
+ JSON = "json"
+ PLAIN_LSON = "plain-lson"
diff --git a/packages/liveblocks-python/liveblocks/models/get_storage_document_response.py b/packages/liveblocks-python/liveblocks/models/get_storage_document_response.py
new file mode 100644
index 0000000000..a215ea9669
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_storage_document_response.py
@@ -0,0 +1,51 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class GetStorageDocumentResponse:
+ """
+ Example:
+ {'liveblocksType': 'LiveObject', 'data': {'aLiveObject': {'liveblocksType': 'LiveObject', 'data': {'a': 1}},
+ 'aLiveList': {'liveblocksType': 'LiveList', 'data': ['a', 'b']}, 'aLiveMap': {'liveblocksType': 'LiveMap',
+ 'data': {'a': 1, 'b': 2}}}}
+
+ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ get_storage_document_response = cls()
+
+ get_storage_document_response.additional_properties = d
+ return get_storage_document_response
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/get_thread_participants_response.py b/packages/liveblocks-python/liveblocks/models/get_thread_participants_response.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/liveblocks-python/liveblocks/models/get_thread_subscriptions_response.py b/packages/liveblocks-python/liveblocks/models/get_thread_subscriptions_response.py
new file mode 100644
index 0000000000..2317a54303
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_thread_subscriptions_response.py
@@ -0,0 +1,57 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.user_subscription import UserSubscription
+
+
+@_attrs_define
+class GetThreadSubscriptionsResponse:
+ """
+ Example:
+ {'data': [{'kind': 'thread', 'subjectId': 'th_abc123', 'createdAt': '2022-07-13T14:32:50.697Z', 'userId':
+ 'alice'}]}
+
+ Attributes:
+ data (list[UserSubscription]):
+ """
+
+ data: list[UserSubscription]
+
+ def to_dict(self) -> dict[str, Any]:
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.user_subscription import UserSubscription
+
+ d = dict(src_dict)
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+ data_item = UserSubscription.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ get_thread_subscriptions_response = cls(
+ data=data,
+ )
+
+ return get_thread_subscriptions_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_threads_response.py b/packages/liveblocks-python/liveblocks/models/get_threads_response.py
new file mode 100644
index 0000000000..eac780933c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_threads_response.py
@@ -0,0 +1,60 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.thread import Thread
+
+
+@_attrs_define
+class GetThreadsResponse:
+ """
+ Example:
+ {'data': [{'type': 'thread', 'id': 'th_abc123', 'roomId': 'my-room-id', 'comments': [{'type': 'comment',
+ 'threadId': 'th_abc123', 'roomId': 'my-room-id', 'id': 'cm_abc123', 'userId': 'alice', 'createdAt':
+ '2022-07-13T14:32:50.697Z', 'body': {'version': 1, 'content': []}, 'metadata': {}, 'reactions': [],
+ 'attachments': []}], 'createdAt': '2022-07-13T14:32:50.697Z', 'updatedAt': '2022-07-13T14:32:50.697Z',
+ 'metadata': {}, 'resolved': False}]}
+
+ Attributes:
+ data (list[Thread]):
+ """
+
+ data: list[Thread]
+
+ def to_dict(self) -> dict[str, Any]:
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.thread import Thread
+
+ d = dict(src_dict)
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+ data_item = Thread.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ get_threads_response = cls(
+ data=data,
+ )
+
+ return get_threads_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_user_groups_response.py b/packages/liveblocks-python/liveblocks/models/get_user_groups_response.py
new file mode 100644
index 0000000000..e4d407878f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_user_groups_response.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.group import Group
+
+
+@_attrs_define
+class GetUserGroupsResponse:
+ """
+ Example:
+ {'data': [{'type': 'group', 'id': 'engineering', 'organizationId': 'org_123456789', 'createdAt':
+ '2024-01-15T10:30:00.000Z', 'updatedAt': '2024-01-15T10:30:00.000Z', 'scopes': {'mention': True}, 'members':
+ [{'id': 'alice', 'addedAt': '2024-01-15T10:30:00.000Z'}]}], 'nextCursor': None}
+
+ Attributes:
+ data (list[Group]):
+ next_cursor (None | str):
+ """
+
+ data: list[Group]
+ next_cursor: None | str
+
+ def to_dict(self) -> dict[str, Any]:
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "data": data,
+ "nextCursor": next_cursor,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.group import Group
+
+ d = dict(src_dict)
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+ data_item = Group.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ get_user_groups_response = cls(
+ data=data,
+ next_cursor=next_cursor,
+ )
+
+ return get_user_groups_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_web_knowledge_source_links_response.py b/packages/liveblocks-python/liveblocks/models/get_web_knowledge_source_links_response.py
new file mode 100644
index 0000000000..045aa4efbf
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_web_knowledge_source_links_response.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.web_knowledge_source_link import WebKnowledgeSourceLink
+
+
+@_attrs_define
+class GetWebKnowledgeSourceLinksResponse:
+ """
+ Example:
+ {'nextCursor': None, 'data': [{'id': 'ksl_abc123', 'url': 'https://docs.example.com/getting-started', 'status':
+ 'ready', 'createdAt': '2024-06-01T12:00:00.000Z', 'lastIndexedAt': '2024-06-01T12:00:00.000Z'}]}
+
+ Attributes:
+ next_cursor (None | str):
+ data (list[WebKnowledgeSourceLink]):
+ """
+
+ next_cursor: None | str
+ data: list[WebKnowledgeSourceLink]
+
+ def to_dict(self) -> dict[str, Any]:
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "nextCursor": next_cursor,
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.web_knowledge_source_link import WebKnowledgeSourceLink
+
+ d = dict(src_dict)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+ data_item = WebKnowledgeSourceLink.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ get_web_knowledge_source_links_response = cls(
+ next_cursor=next_cursor,
+ data=data,
+ )
+
+ return get_web_knowledge_source_links_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_yjs_document_response.py b/packages/liveblocks-python/liveblocks/models/get_yjs_document_response.py
new file mode 100644
index 0000000000..164d23b464
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_yjs_document_response.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class GetYjsDocumentResponse:
+ """
+ Example:
+ {'someYText': 'Contents of YText'}
+
+ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ get_yjs_document_response = cls()
+
+ get_yjs_document_response.additional_properties = d
+ return get_yjs_document_response
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/get_yjs_document_type.py b/packages/liveblocks-python/liveblocks/models/get_yjs_document_type.py
new file mode 100644
index 0000000000..40b925c16e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_yjs_document_type.py
@@ -0,0 +1,9 @@
+from enum import StrEnum
+
+
+class GetYjsDocumentType(StrEnum):
+ YARRAY = "yarray"
+ YMAP = "ymap"
+ YTEXT = "ytext"
+ YXMLFRAGMENT = "yxmlfragment"
+ YXMLTEXT = "yxmltext"
diff --git a/packages/liveblocks-python/liveblocks/models/get_yjs_versions_response.py b/packages/liveblocks-python/liveblocks/models/get_yjs_versions_response.py
new file mode 100644
index 0000000000..d7b338395f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_yjs_versions_response.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.yjs_version import YjsVersion
+
+
+@_attrs_define
+class GetYjsVersionsResponse:
+ """
+ Example:
+ {'data': [{'type': 'historyVersion', 'id': 'vh_abc123', 'createdAt': '2024-10-15T10:30:00.000Z', 'authors':
+ [{'id': 'user-123'}], 'kind': 'yjs'}], 'nextCursor': 'eyJjcmVhdGVkQXQiOiIyMDI0LTEwLTE1VDEwOjMwOjAwLjAwMFoifQ=='}
+
+ Attributes:
+ next_cursor (None | str): Cursor for pagination to get the next page of results
+ data (list[YjsVersion]):
+ """
+
+ next_cursor: None | str
+ data: list[YjsVersion]
+
+ def to_dict(self) -> dict[str, Any]:
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "nextCursor": next_cursor,
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.yjs_version import YjsVersion
+
+ d = dict(src_dict)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+ data_item = YjsVersion.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ get_yjs_versions_response = cls(
+ next_cursor=next_cursor,
+ data=data,
+ )
+
+ return get_yjs_versions_response
diff --git a/packages/liveblocks-python/liveblocks/models/google_model.py b/packages/liveblocks-python/liveblocks/models/google_model.py
new file mode 100644
index 0000000000..d0c2274041
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/google_model.py
@@ -0,0 +1,9 @@
+from enum import StrEnum
+
+
+class GoogleModel(StrEnum):
+ GEMINI_1_5_FLASH = "gemini-1.5-flash"
+ GEMINI_1_5_PRO = "gemini-1.5-pro"
+ GEMINI_2_0_FLASH_001 = "gemini-2.0-flash-001"
+ GEMINI_2_5_FLASH = "gemini-2.5-flash"
+ GEMINI_2_5_PRO = "gemini-2.5-pro"
diff --git a/packages/liveblocks-python/liveblocks/models/google_provider_options.py b/packages/liveblocks-python/liveblocks/models/google_provider_options.py
new file mode 100644
index 0000000000..7a6c5a7726
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/google_provider_options.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.google_provider_options_google import GoogleProviderOptionsGoogle
+
+
+@_attrs_define
+class GoogleProviderOptions:
+ """
+ Example:
+ {'google': {'thinkingConfig': {'thinkingBudget': 10000}}}
+
+ Attributes:
+ google (GoogleProviderOptionsGoogle):
+ """
+
+ google: GoogleProviderOptionsGoogle
+
+ def to_dict(self) -> dict[str, Any]:
+ google = self.google.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "google": google,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.google_provider_options_google import GoogleProviderOptionsGoogle
+
+ d = dict(src_dict)
+ google = GoogleProviderOptionsGoogle.from_dict(d.pop("google"))
+
+ google_provider_options = cls(
+ google=google,
+ )
+
+ return google_provider_options
diff --git a/packages/liveblocks-python/liveblocks/models/google_provider_options_google.py b/packages/liveblocks-python/liveblocks/models/google_provider_options_google.py
new file mode 100644
index 0000000000..c650d1e7c9
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/google_provider_options_google.py
@@ -0,0 +1,71 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.google_provider_options_google_thinking_config import GoogleProviderOptionsGoogleThinkingConfig
+
+
+@_attrs_define
+class GoogleProviderOptionsGoogle:
+ """
+ Attributes:
+ thinking_config (GoogleProviderOptionsGoogleThinkingConfig | Unset):
+ """
+
+ thinking_config: GoogleProviderOptionsGoogleThinkingConfig | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ thinking_config: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.thinking_config, Unset):
+ thinking_config = self.thinking_config.to_dict()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if thinking_config is not UNSET:
+ field_dict["thinkingConfig"] = thinking_config
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.google_provider_options_google_thinking_config import GoogleProviderOptionsGoogleThinkingConfig
+
+ d = dict(src_dict)
+ _thinking_config = d.pop("thinkingConfig", UNSET)
+ thinking_config: GoogleProviderOptionsGoogleThinkingConfig | Unset
+ if isinstance(_thinking_config, Unset):
+ thinking_config = UNSET
+ else:
+ thinking_config = GoogleProviderOptionsGoogleThinkingConfig.from_dict(_thinking_config)
+
+ google_provider_options_google = cls(
+ thinking_config=thinking_config,
+ )
+
+ google_provider_options_google.additional_properties = d
+ return google_provider_options_google
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/google_provider_options_google_thinking_config.py b/packages/liveblocks-python/liveblocks/models/google_provider_options_google_thinking_config.py
new file mode 100644
index 0000000000..97ab6ad2de
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/google_provider_options_google_thinking_config.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class GoogleProviderOptionsGoogleThinkingConfig:
+ """
+ Attributes:
+ thinking_budget (int | Unset):
+ """
+
+ thinking_budget: int | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ thinking_budget = self.thinking_budget
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if thinking_budget is not UNSET:
+ field_dict["thinkingBudget"] = thinking_budget
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ thinking_budget = d.pop("thinkingBudget", UNSET)
+
+ google_provider_options_google_thinking_config = cls(
+ thinking_budget=thinking_budget,
+ )
+
+ google_provider_options_google_thinking_config.additional_properties = d
+ return google_provider_options_google_thinking_config
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/group.py b/packages/liveblocks-python/liveblocks/models/group.py
new file mode 100644
index 0000000000..c585f0ad7d
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/group.py
@@ -0,0 +1,131 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+if TYPE_CHECKING:
+ from ..models.group_member import GroupMember
+ from ..models.group_scopes import GroupScopes
+
+
+@_attrs_define
+class Group:
+ """
+ Example:
+ {'type': 'group', 'id': 'engineering', 'organizationId': 'org_123456789', 'createdAt':
+ '2024-01-15T10:30:00.000Z', 'updatedAt': '2024-01-15T10:30:00.000Z', 'scopes': {'mention': True}, 'members':
+ [{'id': 'alice', 'addedAt': '2024-01-15T10:30:00.000Z'}, {'id': 'bob', 'addedAt': '2024-01-16T09:00:00.000Z'}]}
+
+ Attributes:
+ type_ (Literal['group']):
+ id (str):
+ organization_id (str):
+ created_at (datetime.datetime):
+ updated_at (datetime.datetime):
+ scopes (GroupScopes):
+ members (list[GroupMember]):
+ """
+
+ type_: Literal["group"]
+ id: str
+ organization_id: str
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ scopes: GroupScopes
+ members: list[GroupMember]
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ id = self.id
+
+ organization_id = self.organization_id
+
+ created_at = self.created_at.isoformat()
+
+ updated_at = self.updated_at.isoformat()
+
+ scopes = self.scopes.to_dict()
+
+ members = []
+ for members_item_data in self.members:
+ members_item = members_item_data.to_dict()
+ members.append(members_item)
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "type": type_,
+ "id": id,
+ "organizationId": organization_id,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ "scopes": scopes,
+ "members": members,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.group_member import GroupMember
+ from ..models.group_scopes import GroupScopes
+
+ d = dict(src_dict)
+ type_ = cast(Literal["group"], d.pop("type"))
+ if type_ != "group":
+ raise ValueError(f"type must match const 'group', got '{type_}'")
+
+ id = d.pop("id")
+
+ organization_id = d.pop("organizationId")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ scopes = GroupScopes.from_dict(d.pop("scopes"))
+
+ members = []
+ _members = d.pop("members")
+ for members_item_data in _members:
+ members_item = GroupMember.from_dict(members_item_data)
+
+ members.append(members_item)
+
+ group = cls(
+ type_=type_,
+ id=id,
+ organization_id=organization_id,
+ created_at=created_at,
+ updated_at=updated_at,
+ scopes=scopes,
+ members=members,
+ )
+
+ group.additional_properties = d
+ return group
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/group_member.py b/packages/liveblocks-python/liveblocks/models/group_member.py
new file mode 100644
index 0000000000..27a6c035a6
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/group_member.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+
+@_attrs_define
+class GroupMember:
+ """
+ Example:
+ {'id': 'alice', 'addedAt': '2024-01-15T10:30:00.000Z'}
+
+ Attributes:
+ id (str):
+ added_at (datetime.datetime):
+ """
+
+ id: str
+ added_at: datetime.datetime
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ added_at = self.added_at.isoformat()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "id": id,
+ "addedAt": added_at,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ added_at = isoparse(d.pop("addedAt"))
+
+ group_member = cls(
+ id=id,
+ added_at=added_at,
+ )
+
+ group_member.additional_properties = d
+ return group_member
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/group_scopes.py b/packages/liveblocks-python/liveblocks/models/group_scopes.py
new file mode 100644
index 0000000000..88fe90a6da
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/group_scopes.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class GroupScopes:
+ """
+ Attributes:
+ mention (bool | Unset):
+ """
+
+ mention: bool | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ mention = self.mention
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if mention is not UNSET:
+ field_dict["mention"] = mention
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ mention = d.pop("mention", UNSET)
+
+ group_scopes = cls(
+ mention=mention,
+ )
+
+ group_scopes.additional_properties = d
+ return group_scopes
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/identify_user_request_body.py b/packages/liveblocks-python/liveblocks/models/identify_user_request_body.py
new file mode 100644
index 0000000000..a90f407c46
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/identify_user_request_body.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.identify_user_request_body_user_info import IdentifyUserRequestBodyUserInfo
+
+
+@_attrs_define
+class IdentifyUserRequestBody:
+ """
+ Example:
+ {'userId': 'user-123', 'organizationId': 'acme-corp', 'groupIds': ['marketing', 'engineering'], 'userInfo':
+ {'name': 'bob', 'avatar': 'https://example.org/images/user123.jpg'}}
+
+ Attributes:
+ user_id (str):
+ organization_id (str | Unset):
+ group_ids (list[str] | Unset):
+ user_info (IdentifyUserRequestBodyUserInfo | Unset):
+ """
+
+ user_id: str
+ organization_id: str | Unset = UNSET
+ group_ids: list[str] | Unset = UNSET
+ user_info: IdentifyUserRequestBodyUserInfo | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ organization_id = self.organization_id
+
+ group_ids: list[str] | Unset = UNSET
+ if not isinstance(self.group_ids, Unset):
+ group_ids = self.group_ids
+
+ user_info: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.user_info, Unset):
+ user_info = self.user_info.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ }
+ )
+ if organization_id is not UNSET:
+ field_dict["organizationId"] = organization_id
+ if group_ids is not UNSET:
+ field_dict["groupIds"] = group_ids
+ if user_info is not UNSET:
+ field_dict["userInfo"] = user_info
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.identify_user_request_body_user_info import IdentifyUserRequestBodyUserInfo
+
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ organization_id = d.pop("organizationId", UNSET)
+
+ group_ids = cast(list[str], d.pop("groupIds", UNSET))
+
+ _user_info = d.pop("userInfo", UNSET)
+ user_info: IdentifyUserRequestBodyUserInfo | Unset
+ if isinstance(_user_info, Unset):
+ user_info = UNSET
+ else:
+ user_info = IdentifyUserRequestBodyUserInfo.from_dict(_user_info)
+
+ identify_user_request_body = cls(
+ user_id=user_id,
+ organization_id=organization_id,
+ group_ids=group_ids,
+ user_info=user_info,
+ )
+
+ return identify_user_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/identify_user_request_body_user_info.py b/packages/liveblocks-python/liveblocks/models/identify_user_request_body_user_info.py
new file mode 100644
index 0000000000..f8a22b8fb7
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/identify_user_request_body_user_info.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class IdentifyUserRequestBodyUserInfo:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ identify_user_request_body_user_info = cls()
+
+ identify_user_request_body_user_info.additional_properties = d
+ return identify_user_request_body_user_info
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/identify_user_response.py b/packages/liveblocks-python/liveblocks/models/identify_user_response.py
new file mode 100644
index 0000000000..034ec87c9f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/identify_user_response.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class IdentifyUserResponse:
+ """
+ Example:
+ {'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi...'}
+
+ Attributes:
+ token (str):
+ """
+
+ token: str
+
+ def to_dict(self) -> dict[str, Any]:
+ token = self.token
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "token": token,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ token = d.pop("token")
+
+ identify_user_response = cls(
+ token=token,
+ )
+
+ return identify_user_response
diff --git a/packages/liveblocks-python/liveblocks/models/inbox_notification_activity.py b/packages/liveblocks-python/liveblocks/models/inbox_notification_activity.py
new file mode 100644
index 0000000000..8603fc05f9
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/inbox_notification_activity.py
@@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+if TYPE_CHECKING:
+ from ..models.inbox_notification_activity_data import InboxNotificationActivityData
+
+
+@_attrs_define
+class InboxNotificationActivity:
+ """
+ Example:
+ {'id': 'act_abc123', 'createdAt': '2024-01-15T10:30:00.000Z', 'data': {'url': 'url-to-file'}}
+
+ Attributes:
+ id (str):
+ created_at (datetime.datetime):
+ data (InboxNotificationActivityData):
+ """
+
+ id: str
+ created_at: datetime.datetime
+ data: InboxNotificationActivityData
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ created_at = self.created_at.isoformat()
+
+ data = self.data.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ "createdAt": created_at,
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.inbox_notification_activity_data import InboxNotificationActivityData
+
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ data = InboxNotificationActivityData.from_dict(d.pop("data"))
+
+ inbox_notification_activity = cls(
+ id=id,
+ created_at=created_at,
+ data=data,
+ )
+
+ return inbox_notification_activity
diff --git a/packages/liveblocks-python/liveblocks/models/inbox_notification_activity_data.py b/packages/liveblocks-python/liveblocks/models/inbox_notification_activity_data.py
new file mode 100644
index 0000000000..4f55c5d4b3
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/inbox_notification_activity_data.py
@@ -0,0 +1,56 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class InboxNotificationActivityData:
+ """ """
+
+ additional_properties: dict[str, bool | float | str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ field_dict[prop_name] = prop
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ inbox_notification_activity_data = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+
+ def _parse_additional_property(data: object) -> bool | float | str:
+ return cast(bool | float | str, data)
+
+ additional_property = _parse_additional_property(prop_dict)
+
+ additional_properties[prop_name] = additional_property
+
+ inbox_notification_activity_data.additional_properties = additional_properties
+ return inbox_notification_activity_data
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> bool | float | str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: bool | float | str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/inbox_notification_custom_data.py b/packages/liveblocks-python/liveblocks/models/inbox_notification_custom_data.py
new file mode 100644
index 0000000000..15c3458e7d
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/inbox_notification_custom_data.py
@@ -0,0 +1,141 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.inbox_notification_activity import InboxNotificationActivity
+
+
+@_attrs_define
+class InboxNotificationCustomData:
+ """
+ Example:
+ {'kind': 'custom', 'id': 'in_xyz789', 'subjectId': 'file123', 'roomId': None, 'notifiedAt':
+ '2024-01-15T10:30:00.000Z', 'readAt': None, 'activities': [{'id': 'act_abc123', 'createdAt':
+ '2024-01-15T10:30:00.000Z', 'data': {'url': 'url-to-file'}}]}
+
+ Attributes:
+ id (str):
+ kind (Literal['custom']):
+ subject_id (str):
+ read_at (datetime.datetime | None):
+ notified_at (datetime.datetime):
+ activities (list[InboxNotificationActivity]):
+ room_id (None | str | Unset):
+ """
+
+ id: str
+ kind: Literal["custom"]
+ subject_id: str
+ read_at: datetime.datetime | None
+ notified_at: datetime.datetime
+ activities: list[InboxNotificationActivity]
+ room_id: None | str | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ kind = self.kind
+
+ subject_id = self.subject_id
+
+ read_at: None | str
+ if isinstance(self.read_at, datetime.datetime):
+ read_at = self.read_at.isoformat()
+ else:
+ read_at = self.read_at
+
+ notified_at = self.notified_at.isoformat()
+
+ activities = []
+ for activities_item_data in self.activities:
+ activities_item = activities_item_data.to_dict()
+ activities.append(activities_item)
+
+ room_id: None | str | Unset
+ if isinstance(self.room_id, Unset):
+ room_id = UNSET
+ else:
+ room_id = self.room_id
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ "kind": kind,
+ "subjectId": subject_id,
+ "readAt": read_at,
+ "notifiedAt": notified_at,
+ "activities": activities,
+ }
+ )
+ if room_id is not UNSET:
+ field_dict["roomId"] = room_id
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.inbox_notification_activity import InboxNotificationActivity
+
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ kind = cast(Literal["custom"], d.pop("kind"))
+ if kind != "custom":
+ raise ValueError(f"kind must match const 'custom', got '{kind}'")
+
+ subject_id = d.pop("subjectId")
+
+ def _parse_read_at(data: object) -> datetime.datetime | None:
+ if data is None:
+ return data
+ try:
+ if not isinstance(data, str):
+ raise TypeError()
+ read_at_type_0 = isoparse(data)
+
+ return read_at_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(datetime.datetime | None, data)
+
+ read_at = _parse_read_at(d.pop("readAt"))
+
+ notified_at = isoparse(d.pop("notifiedAt"))
+
+ activities = []
+ _activities = d.pop("activities")
+ for activities_item_data in _activities:
+ activities_item = InboxNotificationActivity.from_dict(activities_item_data)
+
+ activities.append(activities_item)
+
+ def _parse_room_id(data: object) -> None | str | Unset:
+ if data is None:
+ return data
+ if isinstance(data, Unset):
+ return data
+ return cast(None | str | Unset, data)
+
+ room_id = _parse_room_id(d.pop("roomId", UNSET))
+
+ inbox_notification_custom_data = cls(
+ id=id,
+ kind=kind,
+ subject_id=subject_id,
+ read_at=read_at,
+ notified_at=notified_at,
+ activities=activities,
+ room_id=room_id,
+ )
+
+ return inbox_notification_custom_data
diff --git a/packages/liveblocks-python/liveblocks/models/inbox_notification_thread_data.py b/packages/liveblocks-python/liveblocks/models/inbox_notification_thread_data.py
new file mode 100644
index 0000000000..057cee1c0c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/inbox_notification_thread_data.py
@@ -0,0 +1,103 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+
+@_attrs_define
+class InboxNotificationThreadData:
+ """
+ Example:
+ {'kind': 'thread', 'id': 'in_abc123', 'roomId': 'my-room-id', 'threadId': 'th_abc123', 'notifiedAt':
+ '2024-01-15T10:30:00.000Z', 'readAt': None}
+
+ Attributes:
+ id (str):
+ kind (str):
+ thread_id (str):
+ room_id (str):
+ read_at (datetime.datetime | None):
+ notified_at (datetime.datetime):
+ """
+
+ id: str
+ kind: str
+ thread_id: str
+ room_id: str
+ read_at: datetime.datetime | None
+ notified_at: datetime.datetime
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ kind = self.kind
+
+ thread_id = self.thread_id
+
+ room_id = self.room_id
+
+ read_at: None | str
+ if isinstance(self.read_at, datetime.datetime):
+ read_at = self.read_at.isoformat()
+ else:
+ read_at = self.read_at
+
+ notified_at = self.notified_at.isoformat()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ "kind": kind,
+ "threadId": thread_id,
+ "roomId": room_id,
+ "readAt": read_at,
+ "notifiedAt": notified_at,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ kind = d.pop("kind")
+
+ thread_id = d.pop("threadId")
+
+ room_id = d.pop("roomId")
+
+ def _parse_read_at(data: object) -> datetime.datetime | None:
+ if data is None:
+ return data
+ try:
+ if not isinstance(data, str):
+ raise TypeError()
+ read_at_type_0 = isoparse(data)
+
+ return read_at_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(datetime.datetime | None, data)
+
+ read_at = _parse_read_at(d.pop("readAt"))
+
+ notified_at = isoparse(d.pop("notifiedAt"))
+
+ inbox_notification_thread_data = cls(
+ id=id,
+ kind=kind,
+ thread_id=thread_id,
+ room_id=room_id,
+ read_at=read_at,
+ notified_at=notified_at,
+ )
+
+ return inbox_notification_thread_data
diff --git a/packages/liveblocks-python/liveblocks/models/initialize_storage_document_body.py b/packages/liveblocks-python/liveblocks/models/initialize_storage_document_body.py
new file mode 100644
index 0000000000..5c35b450c8
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/initialize_storage_document_body.py
@@ -0,0 +1,82 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.initialize_storage_document_body_data import InitializeStorageDocumentBodyData
+
+
+@_attrs_define
+class InitializeStorageDocumentBody:
+ """
+ Attributes:
+ liveblocks_type (Literal['LiveObject'] | Unset):
+ data (InitializeStorageDocumentBodyData | Unset):
+ """
+
+ liveblocks_type: Literal["LiveObject"] | Unset = UNSET
+ data: InitializeStorageDocumentBodyData | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ liveblocks_type = self.liveblocks_type
+
+ data: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.data, Unset):
+ data = self.data.to_dict()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if liveblocks_type is not UNSET:
+ field_dict["liveblocksType"] = liveblocks_type
+ if data is not UNSET:
+ field_dict["data"] = data
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.initialize_storage_document_body_data import InitializeStorageDocumentBodyData
+
+ d = dict(src_dict)
+ liveblocks_type = cast(Literal["LiveObject"] | Unset, d.pop("liveblocksType", UNSET))
+ if liveblocks_type != "LiveObject" and not isinstance(liveblocks_type, Unset):
+ raise ValueError(f"liveblocksType must match const 'LiveObject', got '{liveblocks_type}'")
+
+ _data = d.pop("data", UNSET)
+ data: InitializeStorageDocumentBodyData | Unset
+ if isinstance(_data, Unset):
+ data = UNSET
+ else:
+ data = InitializeStorageDocumentBodyData.from_dict(_data)
+
+ initialize_storage_document_body = cls(
+ liveblocks_type=liveblocks_type,
+ data=data,
+ )
+
+ initialize_storage_document_body.additional_properties = d
+ return initialize_storage_document_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/initialize_storage_document_body_data.py b/packages/liveblocks-python/liveblocks/models/initialize_storage_document_body_data.py
new file mode 100644
index 0000000000..a0923c32d1
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/initialize_storage_document_body_data.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class InitializeStorageDocumentBodyData:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ initialize_storage_document_body_data = cls()
+
+ initialize_storage_document_body_data.additional_properties = d
+ return initialize_storage_document_body_data
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/initialize_storage_document_response.py b/packages/liveblocks-python/liveblocks/models/initialize_storage_document_response.py
new file mode 100644
index 0000000000..561e2bf6d7
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/initialize_storage_document_response.py
@@ -0,0 +1,86 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.initialize_storage_document_response_data import InitializeStorageDocumentResponseData
+
+
+@_attrs_define
+class InitializeStorageDocumentResponse:
+ """
+ Example:
+ {'liveblocksType': 'LiveObject', 'data': {'aLiveObject': {'liveblocksType': 'LiveObject', 'data': {'a': 1}},
+ 'aLiveList': {'liveblocksType': 'LiveList', 'data': ['a', 'b']}}}
+
+ Attributes:
+ liveblocks_type (Literal['LiveObject'] | Unset):
+ data (InitializeStorageDocumentResponseData | Unset):
+ """
+
+ liveblocks_type: Literal["LiveObject"] | Unset = UNSET
+ data: InitializeStorageDocumentResponseData | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ liveblocks_type = self.liveblocks_type
+
+ data: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.data, Unset):
+ data = self.data.to_dict()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if liveblocks_type is not UNSET:
+ field_dict["liveblocksType"] = liveblocks_type
+ if data is not UNSET:
+ field_dict["data"] = data
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.initialize_storage_document_response_data import InitializeStorageDocumentResponseData
+
+ d = dict(src_dict)
+ liveblocks_type = cast(Literal["LiveObject"] | Unset, d.pop("liveblocksType", UNSET))
+ if liveblocks_type != "LiveObject" and not isinstance(liveblocks_type, Unset):
+ raise ValueError(f"liveblocksType must match const 'LiveObject', got '{liveblocks_type}'")
+
+ _data = d.pop("data", UNSET)
+ data: InitializeStorageDocumentResponseData | Unset
+ if isinstance(_data, Unset):
+ data = UNSET
+ else:
+ data = InitializeStorageDocumentResponseData.from_dict(_data)
+
+ initialize_storage_document_response = cls(
+ liveblocks_type=liveblocks_type,
+ data=data,
+ )
+
+ initialize_storage_document_response.additional_properties = d
+ return initialize_storage_document_response
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/initialize_storage_document_response_data.py b/packages/liveblocks-python/liveblocks/models/initialize_storage_document_response_data.py
new file mode 100644
index 0000000000..3bbe19f38a
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/initialize_storage_document_response_data.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class InitializeStorageDocumentResponseData:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ initialize_storage_document_response_data = cls()
+
+ initialize_storage_document_response_data.additional_properties = d
+ return initialize_storage_document_response_data
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/knowledge_source_base.py b/packages/liveblocks-python/liveblocks/models/knowledge_source_base.py
new file mode 100644
index 0000000000..278278310e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/knowledge_source_base.py
@@ -0,0 +1,90 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..models.knowledge_source_base_status import KnowledgeSourceBaseStatus
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class KnowledgeSourceBase:
+ """
+ Example:
+ {'id': 'ks_abc123', 'createdAt': '2024-06-01T12:00:00.000Z', 'updatedAt': '2024-06-01T12:00:00.000Z',
+ 'lastIndexedAt': '2024-06-01T12:00:00.000Z', 'status': 'ready'}
+
+ Attributes:
+ id (str):
+ created_at (datetime.datetime):
+ updated_at (datetime.datetime):
+ last_indexed_at (datetime.datetime):
+ status (KnowledgeSourceBaseStatus):
+ error_message (str | Unset):
+ """
+
+ id: str
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ last_indexed_at: datetime.datetime
+ status: KnowledgeSourceBaseStatus
+ error_message: str | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ created_at = self.created_at.isoformat()
+
+ updated_at = self.updated_at.isoformat()
+
+ last_indexed_at = self.last_indexed_at.isoformat()
+
+ status = self.status.value
+
+ error_message = self.error_message
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ "lastIndexedAt": last_indexed_at,
+ "status": status,
+ }
+ )
+ if error_message is not UNSET:
+ field_dict["errorMessage"] = error_message
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ last_indexed_at = isoparse(d.pop("lastIndexedAt"))
+
+ status = KnowledgeSourceBaseStatus(d.pop("status"))
+
+ error_message = d.pop("errorMessage", UNSET)
+
+ knowledge_source_base = cls(
+ id=id,
+ created_at=created_at,
+ updated_at=updated_at,
+ last_indexed_at=last_indexed_at,
+ status=status,
+ error_message=error_message,
+ )
+
+ return knowledge_source_base
diff --git a/packages/liveblocks-python/liveblocks/models/knowledge_source_base_status.py b/packages/liveblocks-python/liveblocks/models/knowledge_source_base_status.py
new file mode 100644
index 0000000000..fb3e3c9d65
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/knowledge_source_base_status.py
@@ -0,0 +1,7 @@
+from enum import StrEnum
+
+
+class KnowledgeSourceBaseStatus(StrEnum):
+ ERROR = "error"
+ INGESTING = "ingesting"
+ READY = "ready"
diff --git a/packages/liveblocks-python/liveblocks/models/knowledge_source_file_source.py b/packages/liveblocks-python/liveblocks/models/knowledge_source_file_source.py
new file mode 100644
index 0000000000..80ca8b351e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/knowledge_source_file_source.py
@@ -0,0 +1,133 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+from ..models.knowledge_source_base_status import KnowledgeSourceBaseStatus
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.knowledge_source_file_source_file import KnowledgeSourceFileSourceFile
+
+
+@_attrs_define
+class KnowledgeSourceFileSource:
+ """
+ Example:
+ {'id': 'ks_file123', 'type': 'ai-knowledge-file-source', 'createdAt': '2024-06-01T12:00:00.000Z', 'updatedAt':
+ '2024-06-01T12:00:00.000Z', 'lastIndexedAt': '2024-06-01T12:00:00.000Z', 'status': 'ready', 'file': {'name':
+ 'document.pdf', 'mimeType': 'application/pdf'}}
+
+ Attributes:
+ id (str):
+ created_at (datetime.datetime):
+ updated_at (datetime.datetime):
+ last_indexed_at (datetime.datetime):
+ status (KnowledgeSourceBaseStatus):
+ type_ (Literal['ai-knowledge-file-source']):
+ file (KnowledgeSourceFileSourceFile):
+ error_message (str | Unset):
+ """
+
+ id: str
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ last_indexed_at: datetime.datetime
+ status: KnowledgeSourceBaseStatus
+ type_: Literal["ai-knowledge-file-source"]
+ file: KnowledgeSourceFileSourceFile
+ error_message: str | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ created_at = self.created_at.isoformat()
+
+ updated_at = self.updated_at.isoformat()
+
+ last_indexed_at = self.last_indexed_at.isoformat()
+
+ status = self.status.value
+
+ type_ = self.type_
+
+ file = self.file.to_dict()
+
+ error_message = self.error_message
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "id": id,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ "lastIndexedAt": last_indexed_at,
+ "status": status,
+ "type": type_,
+ "file": file,
+ }
+ )
+ if error_message is not UNSET:
+ field_dict["errorMessage"] = error_message
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.knowledge_source_file_source_file import KnowledgeSourceFileSourceFile
+
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ last_indexed_at = isoparse(d.pop("lastIndexedAt"))
+
+ status = KnowledgeSourceBaseStatus(d.pop("status"))
+
+ type_ = cast(Literal["ai-knowledge-file-source"], d.pop("type"))
+ if type_ != "ai-knowledge-file-source":
+ raise ValueError(f"type must match const 'ai-knowledge-file-source', got '{type_}'")
+
+ file = KnowledgeSourceFileSourceFile.from_dict(d.pop("file"))
+
+ error_message = d.pop("errorMessage", UNSET)
+
+ knowledge_source_file_source = cls(
+ id=id,
+ created_at=created_at,
+ updated_at=updated_at,
+ last_indexed_at=last_indexed_at,
+ status=status,
+ type_=type_,
+ file=file,
+ error_message=error_message,
+ )
+
+ knowledge_source_file_source.additional_properties = d
+ return knowledge_source_file_source
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/knowledge_source_file_source_file.py b/packages/liveblocks-python/liveblocks/models/knowledge_source_file_source_file.py
new file mode 100644
index 0000000000..a0e26bc2e1
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/knowledge_source_file_source_file.py
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class KnowledgeSourceFileSourceFile:
+ """
+ Attributes:
+ name (str):
+ mime_type (str):
+ """
+
+ name: str
+ mime_type: str
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ name = self.name
+
+ mime_type = self.mime_type
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "name": name,
+ "mimeType": mime_type,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ name = d.pop("name")
+
+ mime_type = d.pop("mimeType")
+
+ knowledge_source_file_source_file = cls(
+ name=name,
+ mime_type=mime_type,
+ )
+
+ knowledge_source_file_source_file.additional_properties = d
+ return knowledge_source_file_source_file
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/knowledge_source_web_source.py b/packages/liveblocks-python/liveblocks/models/knowledge_source_web_source.py
new file mode 100644
index 0000000000..6e0c232af5
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/knowledge_source_web_source.py
@@ -0,0 +1,114 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..models.knowledge_source_base_status import KnowledgeSourceBaseStatus
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.knowledge_source_web_source_link import KnowledgeSourceWebSourceLink
+
+
+@_attrs_define
+class KnowledgeSourceWebSource:
+ """
+ Example:
+ {'id': 'ks_web123', 'type': 'ai-knowledge-web-source', 'createdAt': '2024-06-01T12:00:00.000Z', 'updatedAt':
+ '2024-06-01T12:00:00.000Z', 'lastIndexedAt': '2024-06-01T12:00:00.000Z', 'status': 'ready', 'link': {'url':
+ 'https://docs.example.com', 'type': 'crawl'}}
+
+ Attributes:
+ id (str):
+ created_at (datetime.datetime):
+ updated_at (datetime.datetime):
+ last_indexed_at (datetime.datetime):
+ status (KnowledgeSourceBaseStatus):
+ type_ (Literal['ai-knowledge-web-source']):
+ link (KnowledgeSourceWebSourceLink):
+ error_message (str | Unset):
+ """
+
+ id: str
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ last_indexed_at: datetime.datetime
+ status: KnowledgeSourceBaseStatus
+ type_: Literal["ai-knowledge-web-source"]
+ link: KnowledgeSourceWebSourceLink
+ error_message: str | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ created_at = self.created_at.isoformat()
+
+ updated_at = self.updated_at.isoformat()
+
+ last_indexed_at = self.last_indexed_at.isoformat()
+
+ status = self.status.value
+
+ type_ = self.type_
+
+ link = self.link.to_dict()
+
+ error_message = self.error_message
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ "lastIndexedAt": last_indexed_at,
+ "status": status,
+ "type": type_,
+ "link": link,
+ }
+ )
+ if error_message is not UNSET:
+ field_dict["errorMessage"] = error_message
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.knowledge_source_web_source_link import KnowledgeSourceWebSourceLink
+
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ last_indexed_at = isoparse(d.pop("lastIndexedAt"))
+
+ status = KnowledgeSourceBaseStatus(d.pop("status"))
+
+ type_ = cast(Literal["ai-knowledge-web-source"], d.pop("type"))
+ if type_ != "ai-knowledge-web-source":
+ raise ValueError(f"type must match const 'ai-knowledge-web-source', got '{type_}'")
+
+ link = KnowledgeSourceWebSourceLink.from_dict(d.pop("link"))
+
+ error_message = d.pop("errorMessage", UNSET)
+
+ knowledge_source_web_source = cls(
+ id=id,
+ created_at=created_at,
+ updated_at=updated_at,
+ last_indexed_at=last_indexed_at,
+ status=status,
+ type_=type_,
+ link=link,
+ error_message=error_message,
+ )
+
+ return knowledge_source_web_source
diff --git a/packages/liveblocks-python/liveblocks/models/knowledge_source_web_source_link.py b/packages/liveblocks-python/liveblocks/models/knowledge_source_web_source_link.py
new file mode 100644
index 0000000000..6db797941e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/knowledge_source_web_source_link.py
@@ -0,0 +1,69 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..models.knowledge_source_web_source_link_type import KnowledgeSourceWebSourceLinkType
+
+
+@_attrs_define
+class KnowledgeSourceWebSourceLink:
+ """
+ Attributes:
+ url (str):
+ type_ (KnowledgeSourceWebSourceLinkType):
+ """
+
+ url: str
+ type_: KnowledgeSourceWebSourceLinkType
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ url = self.url
+
+ type_ = self.type_.value
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "url": url,
+ "type": type_,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ url = d.pop("url")
+
+ type_ = KnowledgeSourceWebSourceLinkType(d.pop("type"))
+
+ knowledge_source_web_source_link = cls(
+ url=url,
+ type_=type_,
+ )
+
+ knowledge_source_web_source_link.additional_properties = d
+ return knowledge_source_web_source_link
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/knowledge_source_web_source_link_type.py b/packages/liveblocks-python/liveblocks/models/knowledge_source_web_source_link_type.py
new file mode 100644
index 0000000000..86c007a713
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/knowledge_source_web_source_link_type.py
@@ -0,0 +1,7 @@
+from enum import StrEnum
+
+
+class KnowledgeSourceWebSourceLinkType(StrEnum):
+ CRAWL = "crawl"
+ INDIVIDUAL_LINK = "individual_link"
+ SITEMAP = "sitemap"
diff --git a/packages/liveblocks-python/liveblocks/models/management_project.py b/packages/liveblocks-python/liveblocks/models/management_project.py
new file mode 100644
index 0000000000..5e5d9080b7
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/management_project.py
@@ -0,0 +1,173 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+from ..models.management_project_region import ManagementProjectRegion
+from ..models.management_project_type import ManagementProjectType
+
+if TYPE_CHECKING:
+ from ..models.management_project_public_key import ManagementProjectPublicKey
+ from ..models.management_project_secret_key import ManagementProjectSecretKey
+
+
+@_attrs_define
+class ManagementProject:
+ """
+ Example:
+ {'id': '683d49ed6b4d1cec5a597b13', 'teamId': 'team_123', 'type': 'dev', 'name': 'My Project', 'createdAt':
+ '2024-09-03T12:34:56.000Z', 'updatedAt': '2024-09-03T12:34:56.000Z', 'publicKey': {'activated': True,
+ 'createdAt': '2024-09-03T12:34:56.000Z', 'value': 'pk_dev_123'}, 'secretKey': {'createdAt':
+ '2024-09-03T12:34:56.000Z', 'value': 'sk_dev_123'}, 'region': 'earth', 'versionCreationTimeout': False}
+
+ Attributes:
+ id (str):
+ team_id (str):
+ type_ (ManagementProjectType): Example: dev.
+ name (str):
+ created_at (datetime.datetime):
+ updated_at (datetime.datetime):
+ public_key (ManagementProjectPublicKey): Example: {'activated': True, 'createdAt': '2024-09-03T12:34:56.000Z',
+ 'value': 'pk_dev_123'}.
+ secret_key (ManagementProjectSecretKey | None):
+ region (ManagementProjectRegion): Example: earth.
+ version_creation_timeout (bool | int): False to disable timeout or number of seconds between 30 and 300.
+ """
+
+ id: str
+ team_id: str
+ type_: ManagementProjectType
+ name: str
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ public_key: ManagementProjectPublicKey
+ secret_key: ManagementProjectSecretKey | None
+ region: ManagementProjectRegion
+ version_creation_timeout: bool | int
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ from ..models.management_project_secret_key import ManagementProjectSecretKey
+
+ id = self.id
+
+ team_id = self.team_id
+
+ type_ = self.type_.value
+
+ name = self.name
+
+ created_at = self.created_at.isoformat()
+
+ updated_at = self.updated_at.isoformat()
+
+ public_key = self.public_key.to_dict()
+
+ secret_key: dict[str, Any] | None
+ if isinstance(self.secret_key, ManagementProjectSecretKey):
+ secret_key = self.secret_key.to_dict()
+ else:
+ secret_key = self.secret_key
+
+ region = self.region.value
+
+ version_creation_timeout: bool | int
+ version_creation_timeout = self.version_creation_timeout
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "id": id,
+ "teamId": team_id,
+ "type": type_,
+ "name": name,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ "publicKey": public_key,
+ "secretKey": secret_key,
+ "region": region,
+ "versionCreationTimeout": version_creation_timeout,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.management_project_public_key import ManagementProjectPublicKey
+ from ..models.management_project_secret_key import ManagementProjectSecretKey
+
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ team_id = d.pop("teamId")
+
+ type_ = ManagementProjectType(d.pop("type"))
+
+ name = d.pop("name")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ public_key = ManagementProjectPublicKey.from_dict(d.pop("publicKey"))
+
+ def _parse_secret_key(data: object) -> ManagementProjectSecretKey | None:
+ if data is None:
+ return data
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ secret_key_type_0 = ManagementProjectSecretKey.from_dict(data)
+
+ return secret_key_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(ManagementProjectSecretKey | None, data)
+
+ secret_key = _parse_secret_key(d.pop("secretKey"))
+
+ region = ManagementProjectRegion(d.pop("region"))
+
+ def _parse_version_creation_timeout(data: object) -> bool | int:
+ return cast(bool | int, data)
+
+ version_creation_timeout = _parse_version_creation_timeout(d.pop("versionCreationTimeout"))
+
+ management_project = cls(
+ id=id,
+ team_id=team_id,
+ type_=type_,
+ name=name,
+ created_at=created_at,
+ updated_at=updated_at,
+ public_key=public_key,
+ secret_key=secret_key,
+ region=region,
+ version_creation_timeout=version_creation_timeout,
+ )
+
+ management_project.additional_properties = d
+ return management_project
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/management_project_public_key.py b/packages/liveblocks-python/liveblocks/models/management_project_public_key.py
new file mode 100644
index 0000000000..e02aefe183
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/management_project_public_key.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+
+@_attrs_define
+class ManagementProjectPublicKey:
+ """
+ Example:
+ {'activated': True, 'createdAt': '2024-09-03T12:34:56.000Z', 'value': 'pk_dev_123'}
+
+ Attributes:
+ activated (bool):
+ created_at (datetime.datetime):
+ value (str):
+ """
+
+ activated: bool
+ created_at: datetime.datetime
+ value: str
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ activated = self.activated
+
+ created_at = self.created_at.isoformat()
+
+ value = self.value
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "activated": activated,
+ "createdAt": created_at,
+ "value": value,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ activated = d.pop("activated")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ value = d.pop("value")
+
+ management_project_public_key = cls(
+ activated=activated,
+ created_at=created_at,
+ value=value,
+ )
+
+ management_project_public_key.additional_properties = d
+ return management_project_public_key
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/management_project_region.py b/packages/liveblocks-python/liveblocks/models/management_project_region.py
new file mode 100644
index 0000000000..ecd5dcb797
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/management_project_region.py
@@ -0,0 +1,7 @@
+from enum import StrEnum
+
+
+class ManagementProjectRegion(StrEnum):
+ EARTH = "earth"
+ EU = "eu"
+ FEDRAMP = "fedramp"
diff --git a/packages/liveblocks-python/liveblocks/models/management_project_roll_project_secret_api_key_response_secret_key_response.py b/packages/liveblocks-python/liveblocks/models/management_project_roll_project_secret_api_key_response_secret_key_response.py
new file mode 100644
index 0000000000..ab56d70e99
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/management_project_roll_project_secret_api_key_response_secret_key_response.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.management_project_secret_key import ManagementProjectSecretKey
+
+
+@_attrs_define
+class ManagementProjectRollProjectSecretApiKeyResponseSecretKeyResponse:
+ """
+ Example:
+ {'secretKey': {'createdAt': '2024-09-03T12:34:56.000Z', 'value': 'sk_dev_123'}}
+
+ Attributes:
+ secret_key (ManagementProjectSecretKey): Example: {'createdAt': '2024-09-03T12:34:56.000Z', 'value':
+ 'sk_dev_123'}.
+ """
+
+ secret_key: ManagementProjectSecretKey
+
+ def to_dict(self) -> dict[str, Any]:
+ secret_key = self.secret_key.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "secretKey": secret_key,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.management_project_secret_key import ManagementProjectSecretKey
+
+ d = dict(src_dict)
+ secret_key = ManagementProjectSecretKey.from_dict(d.pop("secretKey"))
+
+ management_project_roll_project_secret_api_key_response_secret_key_response = cls(
+ secret_key=secret_key,
+ )
+
+ return management_project_roll_project_secret_api_key_response_secret_key_response
diff --git a/packages/liveblocks-python/liveblocks/models/management_project_secret_key.py b/packages/liveblocks-python/liveblocks/models/management_project_secret_key.py
new file mode 100644
index 0000000000..19b374ab7d
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/management_project_secret_key.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+
+@_attrs_define
+class ManagementProjectSecretKey:
+ """
+ Example:
+ {'createdAt': '2024-09-03T12:34:56.000Z', 'value': 'sk_dev_123'}
+
+ Attributes:
+ created_at (datetime.datetime):
+ value (str):
+ """
+
+ created_at: datetime.datetime
+ value: str
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ created_at = self.created_at.isoformat()
+
+ value = self.value
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "createdAt": created_at,
+ "value": value,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ created_at = isoparse(d.pop("createdAt"))
+
+ value = d.pop("value")
+
+ management_project_secret_key = cls(
+ created_at=created_at,
+ value=value,
+ )
+
+ management_project_secret_key.additional_properties = d
+ return management_project_secret_key
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/management_project_type.py b/packages/liveblocks-python/liveblocks/models/management_project_type.py
new file mode 100644
index 0000000000..218d5881d0
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/management_project_type.py
@@ -0,0 +1,6 @@
+from enum import StrEnum
+
+
+class ManagementProjectType(StrEnum):
+ DEV = "dev"
+ PROD = "prod"
diff --git a/packages/liveblocks-python/liveblocks/models/management_webhook.py b/packages/liveblocks-python/liveblocks/models/management_webhook.py
new file mode 100644
index 0000000000..9492ebeeb5
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/management_webhook.py
@@ -0,0 +1,173 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..models.management_webhook_event import ManagementWebhookEvent
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.management_webhook_additional_headers import ManagementWebhookAdditionalHeaders
+ from ..models.management_webhook_secret import ManagementWebhookSecret
+
+
+@_attrs_define
+class ManagementWebhook:
+ """
+ Example:
+ {'id': 'wh_abc123', 'createdAt': '2024-09-03T12:34:56.000Z', 'updatedAt': '2024-09-03T12:34:56.000Z', 'url':
+ 'https://example.com/webhooks', 'disabled': False, 'subscribedEvents': ['storageUpdated', 'userEntered'],
+ 'secret': {'value': 'whsec_abc123'}, 'storageUpdatedThrottleSeconds': 10, 'yDocUpdatedThrottleSeconds': 10}
+
+ Attributes:
+ id (str):
+ created_at (datetime.datetime):
+ updated_at (datetime.datetime):
+ url (str):
+ disabled (bool):
+ subscribed_events (list[ManagementWebhookEvent]):
+ secret (ManagementWebhookSecret | None):
+ storage_updated_throttle_seconds (int):
+ y_doc_updated_throttle_seconds (int):
+ rate_limit (int | Unset):
+ additional_headers (ManagementWebhookAdditionalHeaders | Unset): Example: {'X-Custom-Header': 'value'}.
+ """
+
+ id: str
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ url: str
+ disabled: bool
+ subscribed_events: list[ManagementWebhookEvent]
+ secret: ManagementWebhookSecret | None
+ storage_updated_throttle_seconds: int
+ y_doc_updated_throttle_seconds: int
+ rate_limit: int | Unset = UNSET
+ additional_headers: ManagementWebhookAdditionalHeaders | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ from ..models.management_webhook_secret import ManagementWebhookSecret
+
+ id = self.id
+
+ created_at = self.created_at.isoformat()
+
+ updated_at = self.updated_at.isoformat()
+
+ url = self.url
+
+ disabled = self.disabled
+
+ subscribed_events = []
+ for subscribed_events_item_data in self.subscribed_events:
+ subscribed_events_item = subscribed_events_item_data.value
+ subscribed_events.append(subscribed_events_item)
+
+ secret: dict[str, Any] | None
+ if isinstance(self.secret, ManagementWebhookSecret):
+ secret = self.secret.to_dict()
+ else:
+ secret = self.secret
+
+ storage_updated_throttle_seconds = self.storage_updated_throttle_seconds
+
+ y_doc_updated_throttle_seconds = self.y_doc_updated_throttle_seconds
+
+ rate_limit = self.rate_limit
+
+ additional_headers: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.additional_headers, Unset):
+ additional_headers = self.additional_headers.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ "url": url,
+ "disabled": disabled,
+ "subscribedEvents": subscribed_events,
+ "secret": secret,
+ "storageUpdatedThrottleSeconds": storage_updated_throttle_seconds,
+ "yDocUpdatedThrottleSeconds": y_doc_updated_throttle_seconds,
+ }
+ )
+ if rate_limit is not UNSET:
+ field_dict["rateLimit"] = rate_limit
+ if additional_headers is not UNSET:
+ field_dict["additionalHeaders"] = additional_headers
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.management_webhook_additional_headers import ManagementWebhookAdditionalHeaders
+ from ..models.management_webhook_secret import ManagementWebhookSecret
+
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ url = d.pop("url")
+
+ disabled = d.pop("disabled")
+
+ subscribed_events = []
+ _subscribed_events = d.pop("subscribedEvents")
+ for subscribed_events_item_data in _subscribed_events:
+ subscribed_events_item = ManagementWebhookEvent(subscribed_events_item_data)
+
+ subscribed_events.append(subscribed_events_item)
+
+ def _parse_secret(data: object) -> ManagementWebhookSecret | None:
+ if data is None:
+ return data
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ secret_type_0 = ManagementWebhookSecret.from_dict(data)
+
+ return secret_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(ManagementWebhookSecret | None, data)
+
+ secret = _parse_secret(d.pop("secret"))
+
+ storage_updated_throttle_seconds = d.pop("storageUpdatedThrottleSeconds")
+
+ y_doc_updated_throttle_seconds = d.pop("yDocUpdatedThrottleSeconds")
+
+ rate_limit = d.pop("rateLimit", UNSET)
+
+ _additional_headers = d.pop("additionalHeaders", UNSET)
+ additional_headers: ManagementWebhookAdditionalHeaders | Unset
+ if isinstance(_additional_headers, Unset):
+ additional_headers = UNSET
+ else:
+ additional_headers = ManagementWebhookAdditionalHeaders.from_dict(_additional_headers)
+
+ management_webhook = cls(
+ id=id,
+ created_at=created_at,
+ updated_at=updated_at,
+ url=url,
+ disabled=disabled,
+ subscribed_events=subscribed_events,
+ secret=secret,
+ storage_updated_throttle_seconds=storage_updated_throttle_seconds,
+ y_doc_updated_throttle_seconds=y_doc_updated_throttle_seconds,
+ rate_limit=rate_limit,
+ additional_headers=additional_headers,
+ )
+
+ return management_webhook
diff --git a/packages/liveblocks-python/liveblocks/models/management_webhook_additional_headers.py b/packages/liveblocks-python/liveblocks/models/management_webhook_additional_headers.py
new file mode 100644
index 0000000000..457bd7411c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/management_webhook_additional_headers.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class ManagementWebhookAdditionalHeaders:
+ """
+ Example:
+ {'X-Custom-Header': 'value'}
+
+ """
+
+ additional_properties: dict[str, str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ management_webhook_additional_headers = cls()
+
+ management_webhook_additional_headers.additional_properties = d
+ return management_webhook_additional_headers
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/management_webhook_event.py b/packages/liveblocks-python/liveblocks/models/management_webhook_event.py
new file mode 100644
index 0000000000..4be3342cb9
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/management_webhook_event.py
@@ -0,0 +1,22 @@
+from enum import StrEnum
+
+
+class ManagementWebhookEvent(StrEnum):
+ COMMENTCREATED = "commentCreated"
+ COMMENTDELETED = "commentDeleted"
+ COMMENTEDITED = "commentEdited"
+ COMMENTMETADATAUPDATED = "commentMetadataUpdated"
+ COMMENTREACTIONADDED = "commentReactionAdded"
+ COMMENTREACTIONREMOVED = "commentReactionRemoved"
+ NOTIFICATION = "notification"
+ ROOMCREATED = "roomCreated"
+ ROOMDELETED = "roomDeleted"
+ STORAGEUPDATED = "storageUpdated"
+ THREADCREATED = "threadCreated"
+ THREADDELETED = "threadDeleted"
+ THREADMARKEDASRESOLVED = "threadMarkedAsResolved"
+ THREADMARKEDASUNRESOLVED = "threadMarkedAsUnresolved"
+ THREADMETADATAUPDATED = "threadMetadataUpdated"
+ USERENTERED = "userEntered"
+ USERLEFT = "userLeft"
+ YDOCUPDATED = "ydocUpdated"
diff --git a/packages/liveblocks-python/liveblocks/models/management_webhook_headers_delete.py b/packages/liveblocks-python/liveblocks/models/management_webhook_headers_delete.py
new file mode 100644
index 0000000000..88067074b9
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/management_webhook_headers_delete.py
@@ -0,0 +1,62 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class ManagementWebhookHeadersDelete:
+ """
+ Example:
+ {'headers': ['X-Custom-Header']}
+
+ Attributes:
+ headers (list[str]):
+ """
+
+ headers: list[str]
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ headers = self.headers
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "headers": headers,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ headers = cast(list[str], d.pop("headers"))
+
+ management_webhook_headers_delete = cls(
+ headers=headers,
+ )
+
+ management_webhook_headers_delete.additional_properties = d
+ return management_webhook_headers_delete
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/management_webhook_secret.py b/packages/liveblocks-python/liveblocks/models/management_webhook_secret.py
new file mode 100644
index 0000000000..6dcb3ed25f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/management_webhook_secret.py
@@ -0,0 +1,62 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class ManagementWebhookSecret:
+ """
+ Example:
+ {'value': 'whsec_abc123'}
+
+ Attributes:
+ value (str):
+ """
+
+ value: str
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ value = self.value
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "value": value,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ value = d.pop("value")
+
+ management_webhook_secret = cls(
+ value=value,
+ )
+
+ management_webhook_secret.additional_properties = d
+ return management_webhook_secret
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/mark_thread_as_resolved_request_body.py b/packages/liveblocks-python/liveblocks/models/mark_thread_as_resolved_request_body.py
new file mode 100644
index 0000000000..d4eeed95e5
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/mark_thread_as_resolved_request_body.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class MarkThreadAsResolvedRequestBody:
+ """
+ Example:
+ {'userId': 'alice'}
+
+ Attributes:
+ user_id (str): The user ID of the user who marked the thread as resolved.
+ """
+
+ user_id: str
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ mark_thread_as_resolved_request_body = cls(
+ user_id=user_id,
+ )
+
+ return mark_thread_as_resolved_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/mark_thread_as_unresolved_request_body.py b/packages/liveblocks-python/liveblocks/models/mark_thread_as_unresolved_request_body.py
new file mode 100644
index 0000000000..2bd495a769
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/mark_thread_as_unresolved_request_body.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class MarkThreadAsUnresolvedRequestBody:
+ """
+ Example:
+ {'userId': 'alice'}
+
+ Attributes:
+ user_id (str): The user ID of the user who marked the thread as unresolved.
+ """
+
+ user_id: str
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ mark_thread_as_unresolved_request_body = cls(
+ user_id=user_id,
+ )
+
+ return mark_thread_as_unresolved_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/move_json_patch_operation.py b/packages/liveblocks-python/liveblocks/models/move_json_patch_operation.py
new file mode 100644
index 0000000000..6bc0a9a324
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/move_json_patch_operation.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class MoveJsonPatchOperation:
+ """
+ Attributes:
+ op (Literal['move']):
+ from_ (str): A JSON Pointer to the source location (RFC 6901). Must start with "/".
+ path (str): A JSON Pointer to the target location (RFC 6901). Must start with "/".
+ """
+
+ op: Literal["move"]
+ from_: str
+ path: str
+
+ def to_dict(self) -> dict[str, Any]:
+ op = self.op
+
+ from_ = self.from_
+
+ path = self.path
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "op": op,
+ "from": from_,
+ "path": path,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ op = cast(Literal["move"], d.pop("op"))
+ if op != "move":
+ raise ValueError(f"op must match const 'move', got '{op}'")
+
+ from_ = d.pop("from")
+
+ path = d.pop("path")
+
+ move_json_patch_operation = cls(
+ op=op,
+ from_=from_,
+ path=path,
+ )
+
+ return move_json_patch_operation
diff --git a/packages/liveblocks-python/liveblocks/models/notification_channel_settings.py b/packages/liveblocks-python/liveblocks/models/notification_channel_settings.py
new file mode 100644
index 0000000000..d0ed5dfc4d
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/notification_channel_settings.py
@@ -0,0 +1,71 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class NotificationChannelSettings:
+ """
+ Example:
+ {'thread': True, 'textMention': False, '$customNotification': True}
+
+ Attributes:
+ thread (bool | Unset):
+ text_mention (bool | Unset):
+ """
+
+ thread: bool | Unset = UNSET
+ text_mention: bool | Unset = UNSET
+ additional_properties: dict[str, bool] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ thread = self.thread
+
+ text_mention = self.text_mention
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if thread is not UNSET:
+ field_dict["thread"] = thread
+ if text_mention is not UNSET:
+ field_dict["textMention"] = text_mention
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ thread = d.pop("thread", UNSET)
+
+ text_mention = d.pop("textMention", UNSET)
+
+ notification_channel_settings = cls(
+ thread=thread,
+ text_mention=text_mention,
+ )
+
+ notification_channel_settings.additional_properties = d
+ return notification_channel_settings
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> bool:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: bool) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/notification_settings.py b/packages/liveblocks-python/liveblocks/models/notification_settings.py
new file mode 100644
index 0000000000..9e621a0da6
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/notification_settings.py
@@ -0,0 +1,110 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.notification_channel_settings import NotificationChannelSettings
+
+
+@_attrs_define
+class NotificationSettings:
+ """Notification settings for each supported channel
+
+ Example:
+ {'email': {'thread': True, 'textMention': False, '$customNotification': True}, 'slack': {'thread': True,
+ 'textMention': True, '$customNotification': False}, 'teams': {'thread': False, 'textMention': True,
+ '$customNotification': True}, 'webPush': {'thread': True, 'textMention': True, '$customNotification': False}}
+
+ Attributes:
+ email (NotificationChannelSettings | Unset): Example: {'thread': True, 'textMention': False,
+ '$customNotification': True}.
+ slack (NotificationChannelSettings | Unset): Example: {'thread': True, 'textMention': False,
+ '$customNotification': True}.
+ teams (NotificationChannelSettings | Unset): Example: {'thread': True, 'textMention': False,
+ '$customNotification': True}.
+ web_push (NotificationChannelSettings | Unset): Example: {'thread': True, 'textMention': False,
+ '$customNotification': True}.
+ """
+
+ email: NotificationChannelSettings | Unset = UNSET
+ slack: NotificationChannelSettings | Unset = UNSET
+ teams: NotificationChannelSettings | Unset = UNSET
+ web_push: NotificationChannelSettings | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ email: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.email, Unset):
+ email = self.email.to_dict()
+
+ slack: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.slack, Unset):
+ slack = self.slack.to_dict()
+
+ teams: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.teams, Unset):
+ teams = self.teams.to_dict()
+
+ web_push: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.web_push, Unset):
+ web_push = self.web_push.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update({})
+ if email is not UNSET:
+ field_dict["email"] = email
+ if slack is not UNSET:
+ field_dict["slack"] = slack
+ if teams is not UNSET:
+ field_dict["teams"] = teams
+ if web_push is not UNSET:
+ field_dict["webPush"] = web_push
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.notification_channel_settings import NotificationChannelSettings
+
+ d = dict(src_dict)
+ _email = d.pop("email", UNSET)
+ email: NotificationChannelSettings | Unset
+ if isinstance(_email, Unset):
+ email = UNSET
+ else:
+ email = NotificationChannelSettings.from_dict(_email)
+
+ _slack = d.pop("slack", UNSET)
+ slack: NotificationChannelSettings | Unset
+ if isinstance(_slack, Unset):
+ slack = UNSET
+ else:
+ slack = NotificationChannelSettings.from_dict(_slack)
+
+ _teams = d.pop("teams", UNSET)
+ teams: NotificationChannelSettings | Unset
+ if isinstance(_teams, Unset):
+ teams = UNSET
+ else:
+ teams = NotificationChannelSettings.from_dict(_teams)
+
+ _web_push = d.pop("webPush", UNSET)
+ web_push: NotificationChannelSettings | Unset
+ if isinstance(_web_push, Unset):
+ web_push = UNSET
+ else:
+ web_push = NotificationChannelSettings.from_dict(_web_push)
+
+ notification_settings = cls(
+ email=email,
+ slack=slack,
+ teams=teams,
+ web_push=web_push,
+ )
+
+ return notification_settings
diff --git a/packages/liveblocks-python/liveblocks/models/open_ai_model.py b/packages/liveblocks-python/liveblocks/models/open_ai_model.py
new file mode 100644
index 0000000000..c251bdc0ea
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/open_ai_model.py
@@ -0,0 +1,23 @@
+from enum import StrEnum
+
+
+class OpenAiModel(StrEnum):
+ GPT_4 = "gpt-4"
+ GPT_4O = "gpt-4o"
+ GPT_4O_MINI = "gpt-4o-mini"
+ GPT_4_1 = "gpt-4.1"
+ GPT_4_1_MINI = "gpt-4.1-mini"
+ GPT_4_1_NANO = "gpt-4.1-nano"
+ GPT_4_TURBO = "gpt-4-turbo"
+ GPT_5 = "gpt-5"
+ GPT_5_1 = "gpt-5.1"
+ GPT_5_1_CHAT_LATEST = "gpt-5.1-chat-latest"
+ GPT_5_1_MINI = "gpt-5.1-mini"
+ GPT_5_CHAT_LATEST = "gpt-5-chat-latest"
+ GPT_5_MINI = "gpt-5-mini"
+ GPT_5_NANO = "gpt-5-nano"
+ O1 = "o1"
+ O1_MINI = "o1-mini"
+ O3 = "o3"
+ O3_MINI = "o3-mini"
+ O4_MINI = "o4-mini"
diff --git a/packages/liveblocks-python/liveblocks/models/open_ai_provider_options.py b/packages/liveblocks-python/liveblocks/models/open_ai_provider_options.py
new file mode 100644
index 0000000000..c09122c1bd
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/open_ai_provider_options.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.open_ai_provider_options_openai import OpenAiProviderOptionsOpenai
+
+
+@_attrs_define
+class OpenAiProviderOptions:
+ """
+ Example:
+ {'openai': {'reasoningEffort': 'medium'}}
+
+ Attributes:
+ openai (OpenAiProviderOptionsOpenai):
+ """
+
+ openai: OpenAiProviderOptionsOpenai
+
+ def to_dict(self) -> dict[str, Any]:
+ openai = self.openai.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "openai": openai,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.open_ai_provider_options_openai import OpenAiProviderOptionsOpenai
+
+ d = dict(src_dict)
+ openai = OpenAiProviderOptionsOpenai.from_dict(d.pop("openai"))
+
+ open_ai_provider_options = cls(
+ openai=openai,
+ )
+
+ return open_ai_provider_options
diff --git a/packages/liveblocks-python/liveblocks/models/open_ai_provider_options_openai.py b/packages/liveblocks-python/liveblocks/models/open_ai_provider_options_openai.py
new file mode 100644
index 0000000000..10efd55157
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/open_ai_provider_options_openai.py
@@ -0,0 +1,69 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+from ..models.open_ai_provider_options_openai_reasoning_effort import OpenAiProviderOptionsOpenaiReasoningEffort
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.open_ai_provider_options_openai_web_search import OpenAiProviderOptionsOpenaiWebSearch
+
+
+@_attrs_define
+class OpenAiProviderOptionsOpenai:
+ """
+ Attributes:
+ reasoning_effort (OpenAiProviderOptionsOpenaiReasoningEffort | Unset):
+ web_search (OpenAiProviderOptionsOpenaiWebSearch | Unset):
+ """
+
+ reasoning_effort: OpenAiProviderOptionsOpenaiReasoningEffort | Unset = UNSET
+ web_search: OpenAiProviderOptionsOpenaiWebSearch | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ reasoning_effort: str | Unset = UNSET
+ if not isinstance(self.reasoning_effort, Unset):
+ reasoning_effort = self.reasoning_effort.value
+
+ web_search: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.web_search, Unset):
+ web_search = self.web_search.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update({})
+ if reasoning_effort is not UNSET:
+ field_dict["reasoningEffort"] = reasoning_effort
+ if web_search is not UNSET:
+ field_dict["webSearch"] = web_search
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.open_ai_provider_options_openai_web_search import OpenAiProviderOptionsOpenaiWebSearch
+
+ d = dict(src_dict)
+ _reasoning_effort = d.pop("reasoningEffort", UNSET)
+ reasoning_effort: OpenAiProviderOptionsOpenaiReasoningEffort | Unset
+ if isinstance(_reasoning_effort, Unset):
+ reasoning_effort = UNSET
+ else:
+ reasoning_effort = OpenAiProviderOptionsOpenaiReasoningEffort(_reasoning_effort)
+
+ _web_search = d.pop("webSearch", UNSET)
+ web_search: OpenAiProviderOptionsOpenaiWebSearch | Unset
+ if isinstance(_web_search, Unset):
+ web_search = UNSET
+ else:
+ web_search = OpenAiProviderOptionsOpenaiWebSearch.from_dict(_web_search)
+
+ open_ai_provider_options_openai = cls(
+ reasoning_effort=reasoning_effort,
+ web_search=web_search,
+ )
+
+ return open_ai_provider_options_openai
diff --git a/packages/liveblocks-python/liveblocks/models/open_ai_provider_options_openai_reasoning_effort.py b/packages/liveblocks-python/liveblocks/models/open_ai_provider_options_openai_reasoning_effort.py
new file mode 100644
index 0000000000..fb3b18af76
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/open_ai_provider_options_openai_reasoning_effort.py
@@ -0,0 +1,7 @@
+from enum import StrEnum
+
+
+class OpenAiProviderOptionsOpenaiReasoningEffort(StrEnum):
+ HIGH = "high"
+ LOW = "low"
+ MEDIUM = "medium"
diff --git a/packages/liveblocks-python/liveblocks/models/open_ai_provider_options_openai_web_search.py b/packages/liveblocks-python/liveblocks/models/open_ai_provider_options_openai_web_search.py
new file mode 100644
index 0000000000..678d2800b6
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/open_ai_provider_options_openai_web_search.py
@@ -0,0 +1,42 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class OpenAiProviderOptionsOpenaiWebSearch:
+ """
+ Attributes:
+ allowed_domains (list[str] | Unset):
+ """
+
+ allowed_domains: list[str] | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ allowed_domains: list[str] | Unset = UNSET
+ if not isinstance(self.allowed_domains, Unset):
+ allowed_domains = self.allowed_domains
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update({})
+ if allowed_domains is not UNSET:
+ field_dict["allowedDomains"] = allowed_domains
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ allowed_domains = cast(list[str], d.pop("allowedDomains", UNSET))
+
+ open_ai_provider_options_openai_web_search = cls(
+ allowed_domains=allowed_domains,
+ )
+
+ return open_ai_provider_options_openai_web_search
diff --git a/packages/liveblocks-python/liveblocks/models/recover_management_webhook_failed_messages_request_body.py b/packages/liveblocks-python/liveblocks/models/recover_management_webhook_failed_messages_request_body.py
new file mode 100644
index 0000000000..0d0bb98d7f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/recover_management_webhook_failed_messages_request_body.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+
+@_attrs_define
+class RecoverManagementWebhookFailedMessagesRequestBody:
+ """
+ Example:
+ {'since': '2026-01-21T00:00:00.000Z'}
+
+ Attributes:
+ since (datetime.datetime):
+ """
+
+ since: datetime.datetime
+
+ def to_dict(self) -> dict[str, Any]:
+ since = self.since.isoformat()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "since": since,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ since = isoparse(d.pop("since"))
+
+ recover_management_webhook_failed_messages_request_body = cls(
+ since=since,
+ )
+
+ return recover_management_webhook_failed_messages_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/remove_comment_reaction_request_body.py b/packages/liveblocks-python/liveblocks/models/remove_comment_reaction_request_body.py
new file mode 100644
index 0000000000..9bfb30c7b5
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/remove_comment_reaction_request_body.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class RemoveCommentReactionRequestBody:
+ """
+ Attributes:
+ user_id (str):
+ emoji (str):
+ removed_at (datetime.datetime | Unset):
+ """
+
+ user_id: str
+ emoji: str
+ removed_at: datetime.datetime | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ emoji = self.emoji
+
+ removed_at: str | Unset = UNSET
+ if not isinstance(self.removed_at, Unset):
+ removed_at = self.removed_at.isoformat()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ "emoji": emoji,
+ }
+ )
+ if removed_at is not UNSET:
+ field_dict["removedAt"] = removed_at
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ emoji = d.pop("emoji")
+
+ _removed_at = d.pop("removedAt", UNSET)
+ removed_at: datetime.datetime | Unset
+ if isinstance(_removed_at, Unset):
+ removed_at = UNSET
+ else:
+ removed_at = isoparse(_removed_at)
+
+ remove_comment_reaction_request_body = cls(
+ user_id=user_id,
+ emoji=emoji,
+ removed_at=removed_at,
+ )
+
+ return remove_comment_reaction_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/remove_group_members_request_body.py b/packages/liveblocks-python/liveblocks/models/remove_group_members_request_body.py
new file mode 100644
index 0000000000..7b47329a46
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/remove_group_members_request_body.py
@@ -0,0 +1,62 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class RemoveGroupMembersRequestBody:
+ """
+ Example:
+ {'memberIds': ['charlie']}
+
+ Attributes:
+ member_ids (list[str]):
+ """
+
+ member_ids: list[str]
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ member_ids = self.member_ids
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "memberIds": member_ids,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ member_ids = cast(list[str], d.pop("memberIds"))
+
+ remove_group_members_request_body = cls(
+ member_ids=member_ids,
+ )
+
+ remove_group_members_request_body.additional_properties = d
+ return remove_group_members_request_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/remove_json_patch_operation.py b/packages/liveblocks-python/liveblocks/models/remove_json_patch_operation.py
new file mode 100644
index 0000000000..7f33d58524
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/remove_json_patch_operation.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class RemoveJsonPatchOperation:
+ """
+ Attributes:
+ op (Literal['remove']):
+ path (str): A JSON Pointer to the target location (RFC 6901). Must start with "/".
+ """
+
+ op: Literal["remove"]
+ path: str
+
+ def to_dict(self) -> dict[str, Any]:
+ op = self.op
+
+ path = self.path
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "op": op,
+ "path": path,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ op = cast(Literal["remove"], d.pop("op"))
+ if op != "remove":
+ raise ValueError(f"op must match const 'remove', got '{op}'")
+
+ path = d.pop("path")
+
+ remove_json_patch_operation = cls(
+ op=op,
+ path=path,
+ )
+
+ return remove_json_patch_operation
diff --git a/packages/liveblocks-python/liveblocks/models/replace_json_patch_operation.py b/packages/liveblocks-python/liveblocks/models/replace_json_patch_operation.py
new file mode 100644
index 0000000000..86424a594e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/replace_json_patch_operation.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class ReplaceJsonPatchOperation:
+ """
+ Attributes:
+ op (Literal['replace'] | Unset):
+ path (str | Unset): A JSON Pointer to the target location (RFC 6901). Must start with "/".
+ value (Any | Unset):
+ """
+
+ op: Literal["replace"] | Unset = UNSET
+ path: str | Unset = UNSET
+ value: Any | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ op = self.op
+
+ path = self.path
+
+ value = self.value
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if op is not UNSET:
+ field_dict["op"] = op
+ if path is not UNSET:
+ field_dict["path"] = path
+ if value is not UNSET:
+ field_dict["value"] = value
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ op = cast(Literal["replace"] | Unset, d.pop("op", UNSET))
+ if op != "replace" and not isinstance(op, Unset):
+ raise ValueError(f"op must match const 'replace', got '{op}'")
+
+ path = d.pop("path", UNSET)
+
+ value = d.pop("value", UNSET)
+
+ replace_json_patch_operation = cls(
+ op=op,
+ path=path,
+ value=value,
+ )
+
+ replace_json_patch_operation.additional_properties = d
+ return replace_json_patch_operation
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/roll_project_public_api_key_request_body.py b/packages/liveblocks-python/liveblocks/models/roll_project_public_api_key_request_body.py
new file mode 100644
index 0000000000..5bfa8eeca7
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/roll_project_public_api_key_request_body.py
@@ -0,0 +1,56 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+from ..models.roll_project_public_api_key_request_body_expiration_in import (
+ RollProjectPublicApiKeyRequestBodyExpirationIn,
+)
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class RollProjectPublicApiKeyRequestBody:
+ """
+ Example:
+ {'expirationIn': '1 hour'}
+
+ Attributes:
+ expiration_in (RollProjectPublicApiKeyRequestBodyExpirationIn | Unset): Default:
+ RollProjectPublicApiKeyRequestBodyExpirationIn.NOW.
+ """
+
+ expiration_in: RollProjectPublicApiKeyRequestBodyExpirationIn | Unset = (
+ RollProjectPublicApiKeyRequestBodyExpirationIn.NOW
+ )
+
+ def to_dict(self) -> dict[str, Any]:
+ expiration_in: str | Unset = UNSET
+ if not isinstance(self.expiration_in, Unset):
+ expiration_in = self.expiration_in.value
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update({})
+ if expiration_in is not UNSET:
+ field_dict["expirationIn"] = expiration_in
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ _expiration_in = d.pop("expirationIn", UNSET)
+ expiration_in: RollProjectPublicApiKeyRequestBodyExpirationIn | Unset
+ if isinstance(_expiration_in, Unset):
+ expiration_in = UNSET
+ else:
+ expiration_in = RollProjectPublicApiKeyRequestBodyExpirationIn(_expiration_in)
+
+ roll_project_public_api_key_request_body = cls(
+ expiration_in=expiration_in,
+ )
+
+ return roll_project_public_api_key_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/roll_project_public_api_key_request_body_expiration_in.py b/packages/liveblocks-python/liveblocks/models/roll_project_public_api_key_request_body_expiration_in.py
new file mode 100644
index 0000000000..3879a02c4c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/roll_project_public_api_key_request_body_expiration_in.py
@@ -0,0 +1,17 @@
+from enum import StrEnum
+
+
+class RollProjectPublicApiKeyRequestBodyExpirationIn(StrEnum):
+ NOW = "now"
+ VALUE_1 = "1h"
+ VALUE_10 = "7d"
+ VALUE_11 = "7days"
+ VALUE_12 = "7 days"
+ VALUE_2 = "1hour"
+ VALUE_3 = "1 hour"
+ VALUE_4 = "24hrs"
+ VALUE_5 = "24hours"
+ VALUE_6 = "24 hours"
+ VALUE_7 = "3d"
+ VALUE_8 = "3days"
+ VALUE_9 = "3 days"
diff --git a/packages/liveblocks-python/liveblocks/models/roll_project_public_api_key_response.py b/packages/liveblocks-python/liveblocks/models/roll_project_public_api_key_response.py
new file mode 100644
index 0000000000..47fc19a33f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/roll_project_public_api_key_response.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.management_project_public_key import ManagementProjectPublicKey
+
+
+@_attrs_define
+class RollProjectPublicApiKeyResponse:
+ """
+ Example:
+ {'publicKey': {'activated': True, 'createdAt': '2024-09-03T12:34:56.000Z', 'value': 'pk_dev_123'}}
+
+ Attributes:
+ public_key (ManagementProjectPublicKey): Example: {'activated': True, 'createdAt': '2024-09-03T12:34:56.000Z',
+ 'value': 'pk_dev_123'}.
+ """
+
+ public_key: ManagementProjectPublicKey
+
+ def to_dict(self) -> dict[str, Any]:
+ public_key = self.public_key.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "publicKey": public_key,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.management_project_public_key import ManagementProjectPublicKey
+
+ d = dict(src_dict)
+ public_key = ManagementProjectPublicKey.from_dict(d.pop("publicKey"))
+
+ roll_project_public_api_key_response = cls(
+ public_key=public_key,
+ )
+
+ return roll_project_public_api_key_response
diff --git a/packages/liveblocks-python/liveblocks/models/roll_project_secret_api_key_request_body.py b/packages/liveblocks-python/liveblocks/models/roll_project_secret_api_key_request_body.py
new file mode 100644
index 0000000000..23ba42d03a
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/roll_project_secret_api_key_request_body.py
@@ -0,0 +1,56 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+from ..models.roll_project_secret_api_key_request_body_expiration_in import (
+ RollProjectSecretApiKeyRequestBodyExpirationIn,
+)
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class RollProjectSecretApiKeyRequestBody:
+ """
+ Example:
+ {'expirationIn': '3 days'}
+
+ Attributes:
+ expiration_in (RollProjectSecretApiKeyRequestBodyExpirationIn | Unset): Default:
+ RollProjectSecretApiKeyRequestBodyExpirationIn.NOW.
+ """
+
+ expiration_in: RollProjectSecretApiKeyRequestBodyExpirationIn | Unset = (
+ RollProjectSecretApiKeyRequestBodyExpirationIn.NOW
+ )
+
+ def to_dict(self) -> dict[str, Any]:
+ expiration_in: str | Unset = UNSET
+ if not isinstance(self.expiration_in, Unset):
+ expiration_in = self.expiration_in.value
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update({})
+ if expiration_in is not UNSET:
+ field_dict["expirationIn"] = expiration_in
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ _expiration_in = d.pop("expirationIn", UNSET)
+ expiration_in: RollProjectSecretApiKeyRequestBodyExpirationIn | Unset
+ if isinstance(_expiration_in, Unset):
+ expiration_in = UNSET
+ else:
+ expiration_in = RollProjectSecretApiKeyRequestBodyExpirationIn(_expiration_in)
+
+ roll_project_secret_api_key_request_body = cls(
+ expiration_in=expiration_in,
+ )
+
+ return roll_project_secret_api_key_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/roll_project_secret_api_key_request_body_expiration_in.py b/packages/liveblocks-python/liveblocks/models/roll_project_secret_api_key_request_body_expiration_in.py
new file mode 100644
index 0000000000..f5cc798ca2
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/roll_project_secret_api_key_request_body_expiration_in.py
@@ -0,0 +1,17 @@
+from enum import StrEnum
+
+
+class RollProjectSecretApiKeyRequestBodyExpirationIn(StrEnum):
+ NOW = "now"
+ VALUE_1 = "1h"
+ VALUE_10 = "7d"
+ VALUE_11 = "7days"
+ VALUE_12 = "7 days"
+ VALUE_2 = "1hour"
+ VALUE_3 = "1 hour"
+ VALUE_4 = "24hrs"
+ VALUE_5 = "24hours"
+ VALUE_6 = "24 hours"
+ VALUE_7 = "3d"
+ VALUE_8 = "3days"
+ VALUE_9 = "3 days"
diff --git a/packages/liveblocks-python/liveblocks/models/room.py b/packages/liveblocks-python/liveblocks/models/room.py
new file mode 100644
index 0000000000..3de5900370
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/room.py
@@ -0,0 +1,140 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..models.room_permission_item import RoomPermissionItem
+from ..models.room_type import RoomType
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.room_accesses import RoomAccesses
+ from ..models.room_metadata import RoomMetadata
+
+
+@_attrs_define
+class Room:
+ """
+ Example:
+ {'type': 'room', 'id': 'my-room-id', 'lastConnectionAt': '2022-08-04T21:07:09.380Z', 'createdAt':
+ '2022-07-13T14:32:50.697Z', 'organizationId': 'org_123456789', 'metadata': {'color': 'blue', 'type':
+ 'whiteboard'}, 'defaultAccesses': ['room:write'], 'groupsAccesses': {'marketing': ['room:write']},
+ 'usersAccesses': {'alice': ['room:write']}}
+
+ Attributes:
+ id (str):
+ type_ (RoomType):
+ organization_id (str):
+ created_at (datetime.datetime):
+ default_accesses (list[RoomPermissionItem]): Example: ['room:read', 'room:presence:write'].
+ users_accesses (RoomAccesses): Example: {'alice': ['room:write'], 'bob': ['room:read', 'room:presence:write']}.
+ groups_accesses (RoomAccesses): Example: {'alice': ['room:write'], 'bob': ['room:read',
+ 'room:presence:write']}.
+ metadata (RoomMetadata): Example: {'color': 'blue', 'type': 'whiteboard'}.
+ last_connection_at (datetime.datetime | Unset):
+ """
+
+ id: str
+ type_: RoomType
+ organization_id: str
+ created_at: datetime.datetime
+ default_accesses: list[RoomPermissionItem]
+ users_accesses: RoomAccesses
+ groups_accesses: RoomAccesses
+ metadata: RoomMetadata
+ last_connection_at: datetime.datetime | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ type_ = self.type_.value
+
+ organization_id = self.organization_id
+
+ created_at = self.created_at.isoformat()
+
+ default_accesses = []
+ for componentsschemas_room_permission_item_data in self.default_accesses:
+ componentsschemas_room_permission_item = componentsschemas_room_permission_item_data.value
+ default_accesses.append(componentsschemas_room_permission_item)
+
+ users_accesses = self.users_accesses.to_dict()
+
+ groups_accesses = self.groups_accesses.to_dict()
+
+ metadata = self.metadata.to_dict()
+
+ last_connection_at: str | Unset = UNSET
+ if not isinstance(self.last_connection_at, Unset):
+ last_connection_at = self.last_connection_at.isoformat()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ "type": type_,
+ "organizationId": organization_id,
+ "createdAt": created_at,
+ "defaultAccesses": default_accesses,
+ "usersAccesses": users_accesses,
+ "groupsAccesses": groups_accesses,
+ "metadata": metadata,
+ }
+ )
+ if last_connection_at is not UNSET:
+ field_dict["lastConnectionAt"] = last_connection_at
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.room_accesses import RoomAccesses
+ from ..models.room_metadata import RoomMetadata
+
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ type_ = RoomType(d.pop("type"))
+
+ organization_id = d.pop("organizationId")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ default_accesses = []
+ _default_accesses = d.pop("defaultAccesses")
+ for componentsschemas_room_permission_item_data in _default_accesses:
+ componentsschemas_room_permission_item = RoomPermissionItem(componentsschemas_room_permission_item_data)
+
+ default_accesses.append(componentsschemas_room_permission_item)
+
+ users_accesses = RoomAccesses.from_dict(d.pop("usersAccesses"))
+
+ groups_accesses = RoomAccesses.from_dict(d.pop("groupsAccesses"))
+
+ metadata = RoomMetadata.from_dict(d.pop("metadata"))
+
+ _last_connection_at = d.pop("lastConnectionAt", UNSET)
+ last_connection_at: datetime.datetime | Unset
+ if isinstance(_last_connection_at, Unset):
+ last_connection_at = UNSET
+ else:
+ last_connection_at = isoparse(_last_connection_at)
+
+ room = cls(
+ id=id,
+ type_=type_,
+ organization_id=organization_id,
+ created_at=created_at,
+ default_accesses=default_accesses,
+ users_accesses=users_accesses,
+ groups_accesses=groups_accesses,
+ metadata=metadata,
+ last_connection_at=last_connection_at,
+ )
+
+ return room
diff --git a/packages/liveblocks-python/liveblocks/models/room_accesses.py b/packages/liveblocks-python/liveblocks/models/room_accesses.py
new file mode 100644
index 0000000000..8f143cc356
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/room_accesses.py
@@ -0,0 +1,66 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..models.room_accesses_additional_property_item import RoomAccessesAdditionalPropertyItem
+
+
+@_attrs_define
+class RoomAccesses:
+ """
+ Example:
+ {'alice': ['room:write'], 'bob': ['room:read', 'room:presence:write']}
+
+ """
+
+ additional_properties: dict[str, list[RoomAccessesAdditionalPropertyItem]] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ field_dict[prop_name] = []
+ for additional_property_item_data in prop:
+ additional_property_item = additional_property_item_data.value
+ field_dict[prop_name].append(additional_property_item)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ room_accesses = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+ additional_property = []
+ _additional_property = prop_dict
+ for additional_property_item_data in _additional_property:
+ additional_property_item = RoomAccessesAdditionalPropertyItem(additional_property_item_data)
+
+ additional_property.append(additional_property_item)
+
+ additional_properties[prop_name] = additional_property
+
+ room_accesses.additional_properties = additional_properties
+ return room_accesses
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> list[RoomAccessesAdditionalPropertyItem]:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: list[RoomAccessesAdditionalPropertyItem]) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/room_accesses_additional_property_item.py b/packages/liveblocks-python/liveblocks/models/room_accesses_additional_property_item.py
new file mode 100644
index 0000000000..ddeb18b240
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/room_accesses_additional_property_item.py
@@ -0,0 +1,8 @@
+from enum import StrEnum
+
+
+class RoomAccessesAdditionalPropertyItem(StrEnum):
+ COMMENTSWRITE = "comments:write"
+ ROOMPRESENCEWRITE = "room:presence:write"
+ ROOMREAD = "room:read"
+ ROOMWRITE = "room:write"
diff --git a/packages/liveblocks-python/liveblocks/models/room_metadata.py b/packages/liveblocks-python/liveblocks/models/room_metadata.py
new file mode 100644
index 0000000000..3b1cf6701c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/room_metadata.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class RoomMetadata:
+ """
+ Example:
+ {'color': 'blue', 'type': 'whiteboard'}
+
+ """
+
+ additional_properties: dict[str, list[str] | str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ if isinstance(prop, list):
+ field_dict[prop_name] = prop
+
+ else:
+ field_dict[prop_name] = prop
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ room_metadata = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+
+ def _parse_additional_property(data: object) -> list[str] | str:
+ try:
+ if not isinstance(data, list):
+ raise TypeError()
+ additional_property_type_1 = cast(list[str], data)
+
+ return additional_property_type_1
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(list[str] | str, data)
+
+ additional_property = _parse_additional_property(prop_dict)
+
+ additional_properties[prop_name] = additional_property
+
+ room_metadata.additional_properties = additional_properties
+ return room_metadata
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> list[str] | str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: list[str] | str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/room_permission_item.py b/packages/liveblocks-python/liveblocks/models/room_permission_item.py
new file mode 100644
index 0000000000..43a2116dc6
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/room_permission_item.py
@@ -0,0 +1,8 @@
+from enum import StrEnum
+
+
+class RoomPermissionItem(StrEnum):
+ COMMENTSWRITE = "comments:write"
+ ROOMPRESENCEWRITE = "room:presence:write"
+ ROOMREAD = "room:read"
+ ROOMWRITE = "room:write"
diff --git a/packages/liveblocks-python/liveblocks/models/room_subscription_settings.py b/packages/liveblocks-python/liveblocks/models/room_subscription_settings.py
new file mode 100644
index 0000000000..c353a0eb99
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/room_subscription_settings.py
@@ -0,0 +1,54 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+from ..models.room_subscription_settings_text_mentions import RoomSubscriptionSettingsTextMentions
+from ..models.room_subscription_settings_threads import RoomSubscriptionSettingsThreads
+
+
+@_attrs_define
+class RoomSubscriptionSettings:
+ """
+ Example:
+ {'threads': 'all', 'textMentions': 'mine'}
+
+ Attributes:
+ threads (RoomSubscriptionSettingsThreads):
+ text_mentions (RoomSubscriptionSettingsTextMentions):
+ """
+
+ threads: RoomSubscriptionSettingsThreads
+ text_mentions: RoomSubscriptionSettingsTextMentions
+
+ def to_dict(self) -> dict[str, Any]:
+ threads = self.threads.value
+
+ text_mentions = self.text_mentions.value
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "threads": threads,
+ "textMentions": text_mentions,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ threads = RoomSubscriptionSettingsThreads(d.pop("threads"))
+
+ text_mentions = RoomSubscriptionSettingsTextMentions(d.pop("textMentions"))
+
+ room_subscription_settings = cls(
+ threads=threads,
+ text_mentions=text_mentions,
+ )
+
+ return room_subscription_settings
diff --git a/packages/liveblocks-python/liveblocks/models/room_subscription_settings_text_mentions.py b/packages/liveblocks-python/liveblocks/models/room_subscription_settings_text_mentions.py
new file mode 100644
index 0000000000..6195d5fa69
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/room_subscription_settings_text_mentions.py
@@ -0,0 +1,6 @@
+from enum import StrEnum
+
+
+class RoomSubscriptionSettingsTextMentions(StrEnum):
+ MINE = "mine"
+ NONE = "none"
diff --git a/packages/liveblocks-python/liveblocks/models/room_subscription_settings_threads.py b/packages/liveblocks-python/liveblocks/models/room_subscription_settings_threads.py
new file mode 100644
index 0000000000..caaa256086
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/room_subscription_settings_threads.py
@@ -0,0 +1,7 @@
+from enum import StrEnum
+
+
+class RoomSubscriptionSettingsThreads(StrEnum):
+ ALL = "all"
+ NONE = "none"
+ REPLIES_AND_MENTIONS = "replies_and_mentions"
diff --git a/packages/liveblocks-python/liveblocks/models/room_type.py b/packages/liveblocks-python/liveblocks/models/room_type.py
new file mode 100644
index 0000000000..ca36e5d2d7
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/room_type.py
@@ -0,0 +1,5 @@
+from enum import StrEnum
+
+
+class RoomType(StrEnum):
+ ROOM = "room"
diff --git a/packages/liveblocks-python/liveblocks/models/rotate_management_webhook_secret_response.py b/packages/liveblocks-python/liveblocks/models/rotate_management_webhook_secret_response.py
new file mode 100644
index 0000000000..f386c84652
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/rotate_management_webhook_secret_response.py
@@ -0,0 +1,75 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+if TYPE_CHECKING:
+ from ..models.management_webhook_secret import ManagementWebhookSecret
+
+
+@_attrs_define
+class RotateManagementWebhookSecretResponse:
+ """
+ Example:
+ {'secret': {'value': 'whsec_new_abc123'}, 'message': 'Previous secret remains valid for 24 hours.'}
+
+ Attributes:
+ secret (ManagementWebhookSecret): Example: {'value': 'whsec_abc123'}.
+ message (str):
+ """
+
+ secret: ManagementWebhookSecret
+ message: str
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ secret = self.secret.to_dict()
+
+ message = self.message
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "secret": secret,
+ "message": message,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.management_webhook_secret import ManagementWebhookSecret
+
+ d = dict(src_dict)
+ secret = ManagementWebhookSecret.from_dict(d.pop("secret"))
+
+ message = d.pop("message")
+
+ rotate_management_webhook_secret_response = cls(
+ secret=secret,
+ message=message,
+ )
+
+ rotate_management_webhook_secret_response.additional_properties = d
+ return rotate_management_webhook_secret_response
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/set_presence_request_body.py b/packages/liveblocks-python/liveblocks/models/set_presence_request_body.py
new file mode 100644
index 0000000000..8d0f626a59
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/set_presence_request_body.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.set_presence_request_body_data import SetPresenceRequestBodyData
+ from ..models.set_presence_request_body_user_info import SetPresenceRequestBodyUserInfo
+
+
+@_attrs_define
+class SetPresenceRequestBody:
+ """
+ Example:
+ {'userId': 'agent-123', 'data': {'status': 'active', 'cursor': {'x': 100, 'y': 200}}, 'userInfo': {'name': 'AI
+ Assistant', 'avatar': 'https://example.org/images/agent123.jpg'}, 'ttl': 60}
+
+ Attributes:
+ user_id (str): ID of the user to set presence for
+ data (SetPresenceRequestBodyData): Presence data as a JSON object
+ user_info (SetPresenceRequestBodyUserInfo | Unset): Metadata about the user or agent
+ ttl (int | Unset): Time-to-live in seconds (minimum: 2, maximum: 3599). After this duration, the presence will
+ automatically expire.
+ """
+
+ user_id: str
+ data: SetPresenceRequestBodyData
+ user_info: SetPresenceRequestBodyUserInfo | Unset = UNSET
+ ttl: int | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ data = self.data.to_dict()
+
+ user_info: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.user_info, Unset):
+ user_info = self.user_info.to_dict()
+
+ ttl = self.ttl
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ "data": data,
+ }
+ )
+ if user_info is not UNSET:
+ field_dict["userInfo"] = user_info
+ if ttl is not UNSET:
+ field_dict["ttl"] = ttl
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.set_presence_request_body_data import SetPresenceRequestBodyData
+ from ..models.set_presence_request_body_user_info import SetPresenceRequestBodyUserInfo
+
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ data = SetPresenceRequestBodyData.from_dict(d.pop("data"))
+
+ _user_info = d.pop("userInfo", UNSET)
+ user_info: SetPresenceRequestBodyUserInfo | Unset
+ if isinstance(_user_info, Unset):
+ user_info = UNSET
+ else:
+ user_info = SetPresenceRequestBodyUserInfo.from_dict(_user_info)
+
+ ttl = d.pop("ttl", UNSET)
+
+ set_presence_request_body = cls(
+ user_id=user_id,
+ data=data,
+ user_info=user_info,
+ ttl=ttl,
+ )
+
+ return set_presence_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/set_presence_request_body_data.py b/packages/liveblocks-python/liveblocks/models/set_presence_request_body_data.py
new file mode 100644
index 0000000000..0fcb354686
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/set_presence_request_body_data.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class SetPresenceRequestBodyData:
+ """Presence data as a JSON object"""
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ set_presence_request_body_data = cls()
+
+ set_presence_request_body_data.additional_properties = d
+ return set_presence_request_body_data
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/set_presence_request_body_user_info.py b/packages/liveblocks-python/liveblocks/models/set_presence_request_body_user_info.py
new file mode 100644
index 0000000000..e3a1287d08
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/set_presence_request_body_user_info.py
@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class SetPresenceRequestBodyUserInfo:
+ """Metadata about the user or agent
+
+ Attributes:
+ name (str | Unset): Optional name for the user or agent
+ avatar (str | Unset): Optional avatar URL for the user
+ color (str | Unset): Optional color for the user
+ """
+
+ name: str | Unset = UNSET
+ avatar: str | Unset = UNSET
+ color: str | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ name = self.name
+
+ avatar = self.avatar
+
+ color = self.color
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if name is not UNSET:
+ field_dict["name"] = name
+ if avatar is not UNSET:
+ field_dict["avatar"] = avatar
+ if color is not UNSET:
+ field_dict["color"] = color
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ name = d.pop("name", UNSET)
+
+ avatar = d.pop("avatar", UNSET)
+
+ color = d.pop("color", UNSET)
+
+ set_presence_request_body_user_info = cls(
+ name=name,
+ avatar=avatar,
+ color=color,
+ )
+
+ set_presence_request_body_user_info.additional_properties = d
+ return set_presence_request_body_user_info
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/subscribe_to_thread_request_body.py b/packages/liveblocks-python/liveblocks/models/subscribe_to_thread_request_body.py
new file mode 100644
index 0000000000..c953f07a56
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/subscribe_to_thread_request_body.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class SubscribeToThreadRequestBody:
+ """
+ Example:
+ {'userId': 'alice'}
+
+ Attributes:
+ user_id (str):
+ """
+
+ user_id: str
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ subscribe_to_thread_request_body = cls(
+ user_id=user_id,
+ )
+
+ return subscribe_to_thread_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/subscription.py b/packages/liveblocks-python/liveblocks/models/subscription.py
new file mode 100644
index 0000000000..62a65ed3cf
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/subscription.py
@@ -0,0 +1,61 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+
+@_attrs_define
+class Subscription:
+ """
+ Example:
+ {'kind': 'thread', 'subjectId': 'th_abc123', 'createdAt': '2022-07-13T14:32:50.697Z'}
+
+ Attributes:
+ kind (str):
+ subject_id (str):
+ created_at (datetime.datetime):
+ """
+
+ kind: str
+ subject_id: str
+ created_at: datetime.datetime
+
+ def to_dict(self) -> dict[str, Any]:
+ kind = self.kind
+
+ subject_id = self.subject_id
+
+ created_at = self.created_at.isoformat()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "kind": kind,
+ "subjectId": subject_id,
+ "createdAt": created_at,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ kind = d.pop("kind")
+
+ subject_id = d.pop("subjectId")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ subscription = cls(
+ kind=kind,
+ subject_id=subject_id,
+ created_at=created_at,
+ )
+
+ return subscription
diff --git a/packages/liveblocks-python/liveblocks/models/test_json_patch_operation.py b/packages/liveblocks-python/liveblocks/models/test_json_patch_operation.py
new file mode 100644
index 0000000000..ef91312cbb
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/test_json_patch_operation.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class TestJsonPatchOperation:
+ """
+ Attributes:
+ op (Literal['test']):
+ path (str): A JSON Pointer to the target location (RFC 6901). Must start with "/".
+ value (Any):
+ """
+
+ op: Literal["test"]
+ path: str
+ value: Any
+
+ def to_dict(self) -> dict[str, Any]:
+ op = self.op
+
+ path = self.path
+
+ value = self.value
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "op": op,
+ "path": path,
+ "value": value,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ op = cast(Literal["test"], d.pop("op"))
+ if op != "test":
+ raise ValueError(f"op must match const 'test', got '{op}'")
+
+ path = d.pop("path")
+
+ value = d.pop("value")
+
+ test_json_patch_operation = cls(
+ op=op,
+ path=path,
+ value=value,
+ )
+
+ return test_json_patch_operation
diff --git a/packages/liveblocks-python/liveblocks/models/test_management_webhook_request_body.py b/packages/liveblocks-python/liveblocks/models/test_management_webhook_request_body.py
new file mode 100644
index 0000000000..fe4853869c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/test_management_webhook_request_body.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..models.management_webhook_event import ManagementWebhookEvent
+
+
+@_attrs_define
+class TestManagementWebhookRequestBody:
+ """
+ Example:
+ {'subscribedEvent': 'storageUpdated'}
+
+ Attributes:
+ subscribed_event (ManagementWebhookEvent): Example: storageUpdated.
+ """
+
+ subscribed_event: ManagementWebhookEvent
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ subscribed_event = self.subscribed_event.value
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "subscribedEvent": subscribed_event,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ subscribed_event = ManagementWebhookEvent(d.pop("subscribedEvent"))
+
+ test_management_webhook_request_body = cls(
+ subscribed_event=subscribed_event,
+ )
+
+ test_management_webhook_request_body.additional_properties = d
+ return test_management_webhook_request_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/test_management_webhook_response.py b/packages/liveblocks-python/liveblocks/models/test_management_webhook_response.py
new file mode 100644
index 0000000000..dd2442ea62
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/test_management_webhook_response.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.test_management_webhook_response_message import TestManagementWebhookResponseMessage
+
+
+@_attrs_define
+class TestManagementWebhookResponse:
+ """
+ Example:
+ {'message': {'id': 'msg_abc123', 'deliveredAt': '2024-09-03T12:34:56.000Z'}}
+
+ Attributes:
+ message (TestManagementWebhookResponseMessage):
+ """
+
+ message: TestManagementWebhookResponseMessage
+
+ def to_dict(self) -> dict[str, Any]:
+ message = self.message.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "message": message,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.test_management_webhook_response_message import TestManagementWebhookResponseMessage
+
+ d = dict(src_dict)
+ message = TestManagementWebhookResponseMessage.from_dict(d.pop("message"))
+
+ test_management_webhook_response = cls(
+ message=message,
+ )
+
+ return test_management_webhook_response
diff --git a/packages/liveblocks-python/liveblocks/models/test_management_webhook_response_message.py b/packages/liveblocks-python/liveblocks/models/test_management_webhook_response_message.py
new file mode 100644
index 0000000000..7ed9f6f265
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/test_management_webhook_response_message.py
@@ -0,0 +1,74 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class TestManagementWebhookResponseMessage:
+ """
+ Attributes:
+ id (str):
+ delivered_at (datetime.datetime | None | Unset):
+ """
+
+ id: str
+ delivered_at: datetime.datetime | None | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ delivered_at: None | str | Unset
+ if isinstance(self.delivered_at, Unset):
+ delivered_at = UNSET
+ elif isinstance(self.delivered_at, datetime.datetime):
+ delivered_at = self.delivered_at.isoformat()
+ else:
+ delivered_at = self.delivered_at
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ }
+ )
+ if delivered_at is not UNSET:
+ field_dict["deliveredAt"] = delivered_at
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ def _parse_delivered_at(data: object) -> datetime.datetime | None | Unset:
+ if data is None:
+ return data
+ if isinstance(data, Unset):
+ return data
+ try:
+ if not isinstance(data, str):
+ raise TypeError()
+ delivered_at_type_0 = isoparse(data)
+
+ return delivered_at_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(datetime.datetime | None | Unset, data)
+
+ delivered_at = _parse_delivered_at(d.pop("deliveredAt", UNSET))
+
+ test_management_webhook_response_message = cls(
+ id=id,
+ delivered_at=delivered_at,
+ )
+
+ return test_management_webhook_response_message
diff --git a/packages/liveblocks-python/liveblocks/models/thread.py b/packages/liveblocks-python/liveblocks/models/thread.py
new file mode 100644
index 0000000000..bbc9732aa4
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/thread.py
@@ -0,0 +1,142 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+if TYPE_CHECKING:
+ from ..models.comment import Comment
+ from ..models.thread_metadata import ThreadMetadata
+
+
+@_attrs_define
+class Thread:
+ """
+ Example:
+ {'type': 'thread', 'id': 'th_abc123', 'roomId': 'my-room-id', 'comments': [{'type': 'comment', 'threadId':
+ 'th_abc123', 'roomId': 'my-room-id', 'id': 'cm_abc123', 'userId': 'alice', 'createdAt':
+ '2022-07-13T14:32:50.697Z', 'body': {'version': 1, 'content': []}, 'metadata': {}, 'reactions': [],
+ 'attachments': []}], 'createdAt': '2022-07-13T14:32:50.697Z', 'updatedAt': '2022-07-13T14:32:50.697Z',
+ 'metadata': {'color': 'blue'}, 'resolved': False}
+
+ Attributes:
+ type_ (Literal['thread']):
+ id (str):
+ room_id (str):
+ comments (list[Comment]):
+ created_at (datetime.datetime):
+ metadata (ThreadMetadata): Custom metadata attached to a thread. Supports maximum 50 entries. Key length has a
+ limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings.
+ resolved (bool):
+ updated_at (datetime.datetime):
+ """
+
+ type_: Literal["thread"]
+ id: str
+ room_id: str
+ comments: list[Comment]
+ created_at: datetime.datetime
+ metadata: ThreadMetadata
+ resolved: bool
+ updated_at: datetime.datetime
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ type_ = self.type_
+
+ id = self.id
+
+ room_id = self.room_id
+
+ comments = []
+ for comments_item_data in self.comments:
+ comments_item = comments_item_data.to_dict()
+ comments.append(comments_item)
+
+ created_at = self.created_at.isoformat()
+
+ metadata = self.metadata.to_dict()
+
+ resolved = self.resolved
+
+ updated_at = self.updated_at.isoformat()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "type": type_,
+ "id": id,
+ "roomId": room_id,
+ "comments": comments,
+ "createdAt": created_at,
+ "metadata": metadata,
+ "resolved": resolved,
+ "updatedAt": updated_at,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.comment import Comment
+ from ..models.thread_metadata import ThreadMetadata
+
+ d = dict(src_dict)
+ type_ = cast(Literal["thread"], d.pop("type"))
+ if type_ != "thread":
+ raise ValueError(f"type must match const 'thread', got '{type_}'")
+
+ id = d.pop("id")
+
+ room_id = d.pop("roomId")
+
+ comments = []
+ _comments = d.pop("comments")
+ for comments_item_data in _comments:
+ comments_item = Comment.from_dict(comments_item_data)
+
+ comments.append(comments_item)
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ metadata = ThreadMetadata.from_dict(d.pop("metadata"))
+
+ resolved = d.pop("resolved")
+
+ updated_at = isoparse(d.pop("updatedAt"))
+
+ thread = cls(
+ type_=type_,
+ id=id,
+ room_id=room_id,
+ comments=comments,
+ created_at=created_at,
+ metadata=metadata,
+ resolved=resolved,
+ updated_at=updated_at,
+ )
+
+ thread.additional_properties = d
+ return thread
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/thread_metadata.py b/packages/liveblocks-python/liveblocks/models/thread_metadata.py
new file mode 100644
index 0000000000..03d4b6ece0
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/thread_metadata.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class ThreadMetadata:
+ """Custom metadata attached to a thread. Supports maximum 50 entries. Key length has a limit of 40 characters maximum.
+ Value length has a limit of 4000 characters maximum for strings.
+
+ """
+
+ additional_properties: dict[str, bool | float | str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ field_dict[prop_name] = prop
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ thread_metadata = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+
+ def _parse_additional_property(data: object) -> bool | float | str:
+ return cast(bool | float | str, data)
+
+ additional_property = _parse_additional_property(prop_dict)
+
+ additional_properties[prop_name] = additional_property
+
+ thread_metadata.additional_properties = additional_properties
+ return thread_metadata
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> bool | float | str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: bool | float | str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/trigger_inbox_notification_request_body.py b/packages/liveblocks-python/liveblocks/models/trigger_inbox_notification_request_body.py
new file mode 100644
index 0000000000..59b9d9aed3
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/trigger_inbox_notification_request_body.py
@@ -0,0 +1,115 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.trigger_inbox_notification_request_body_activity_data import (
+ TriggerInboxNotificationRequestBodyActivityData,
+ )
+
+
+@_attrs_define
+class TriggerInboxNotificationRequestBody:
+ """
+ Example:
+ {'userId': 'alice', 'kind': 'file-uploaded', 'subjectId': 'file123', 'activityData': {'url': 'url-to-file'}}
+
+ Attributes:
+ user_id (str):
+ kind (str):
+ subject_id (str):
+ activity_data (TriggerInboxNotificationRequestBodyActivityData):
+ room_id (str | Unset):
+ organization_id (str | Unset):
+ """
+
+ user_id: str
+ kind: str
+ subject_id: str
+ activity_data: TriggerInboxNotificationRequestBodyActivityData
+ room_id: str | Unset = UNSET
+ organization_id: str | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ kind = self.kind
+
+ subject_id = self.subject_id
+
+ activity_data = self.activity_data.to_dict()
+
+ room_id = self.room_id
+
+ organization_id = self.organization_id
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "userId": user_id,
+ "kind": kind,
+ "subjectId": subject_id,
+ "activityData": activity_data,
+ }
+ )
+ if room_id is not UNSET:
+ field_dict["roomId"] = room_id
+ if organization_id is not UNSET:
+ field_dict["organizationId"] = organization_id
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.trigger_inbox_notification_request_body_activity_data import (
+ TriggerInboxNotificationRequestBodyActivityData,
+ )
+
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ kind = d.pop("kind")
+
+ subject_id = d.pop("subjectId")
+
+ activity_data = TriggerInboxNotificationRequestBodyActivityData.from_dict(d.pop("activityData"))
+
+ room_id = d.pop("roomId", UNSET)
+
+ organization_id = d.pop("organizationId", UNSET)
+
+ trigger_inbox_notification_request_body = cls(
+ user_id=user_id,
+ kind=kind,
+ subject_id=subject_id,
+ activity_data=activity_data,
+ room_id=room_id,
+ organization_id=organization_id,
+ )
+
+ trigger_inbox_notification_request_body.additional_properties = d
+ return trigger_inbox_notification_request_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/trigger_inbox_notification_request_body_activity_data.py b/packages/liveblocks-python/liveblocks/models/trigger_inbox_notification_request_body_activity_data.py
new file mode 100644
index 0000000000..8e016dd24e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/trigger_inbox_notification_request_body_activity_data.py
@@ -0,0 +1,56 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class TriggerInboxNotificationRequestBodyActivityData:
+ """ """
+
+ additional_properties: dict[str, bool | float | str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ field_dict[prop_name] = prop
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ trigger_inbox_notification_request_body_activity_data = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+
+ def _parse_additional_property(data: object) -> bool | float | str:
+ return cast(bool | float | str, data)
+
+ additional_property = _parse_additional_property(prop_dict)
+
+ additional_properties[prop_name] = additional_property
+
+ trigger_inbox_notification_request_body_activity_data.additional_properties = additional_properties
+ return trigger_inbox_notification_request_body_activity_data
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> bool | float | str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: bool | float | str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/unsubscribe_from_thread_request_body.py b/packages/liveblocks-python/liveblocks/models/unsubscribe_from_thread_request_body.py
new file mode 100644
index 0000000000..1fe10d48bb
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/unsubscribe_from_thread_request_body.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class UnsubscribeFromThreadRequestBody:
+ """
+ Example:
+ {'userId': 'alice'}
+
+ Attributes:
+ user_id (str):
+ """
+
+ user_id: str
+
+ def to_dict(self) -> dict[str, Any]:
+ user_id = self.user_id
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "userId": user_id,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ user_id = d.pop("userId")
+
+ unsubscribe_from_thread_request_body = cls(
+ user_id=user_id,
+ )
+
+ return unsubscribe_from_thread_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/update_ai_copilot_request_body.py b/packages/liveblocks-python/liveblocks/models/update_ai_copilot_request_body.py
new file mode 100644
index 0000000000..7cafc057f6
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_ai_copilot_request_body.py
@@ -0,0 +1,253 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+from ..models.update_ai_copilot_request_body_provider import UpdateAiCopilotRequestBodyProvider
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.anthropic_provider_options import AnthropicProviderOptions
+ from ..models.google_provider_options import GoogleProviderOptions
+ from ..models.open_ai_provider_options import OpenAiProviderOptions
+
+
+@_attrs_define
+class UpdateAiCopilotRequestBody:
+ """
+ Example:
+ {'name': 'Updated Copilot', 'systemPrompt': 'You are an updated helpful assistant.', 'providerModel': 'gpt-4o',
+ 'settings': {'maxTokens': 8192}}
+
+ Attributes:
+ name (str | Unset):
+ description (None | str | Unset):
+ system_prompt (str | Unset):
+ knowledge_prompt (None | str | Unset):
+ always_use_knowledge (bool | Unset):
+ settings (AiCopilotProviderSettings | None | Unset):
+ provider_api_key (str | Unset):
+ provider (UpdateAiCopilotRequestBodyProvider | Unset):
+ provider_model (str | Unset):
+ provider_options (AnthropicProviderOptions | GoogleProviderOptions | None | OpenAiProviderOptions | Unset):
+ compatible_provider_name (str | Unset):
+ provider_base_url (str | Unset):
+ """
+
+ name: str | Unset = UNSET
+ description: None | str | Unset = UNSET
+ system_prompt: str | Unset = UNSET
+ knowledge_prompt: None | str | Unset = UNSET
+ always_use_knowledge: bool | Unset = UNSET
+ settings: AiCopilotProviderSettings | None | Unset = UNSET
+ provider_api_key: str | Unset = UNSET
+ provider: UpdateAiCopilotRequestBodyProvider | Unset = UNSET
+ provider_model: str | Unset = UNSET
+ provider_options: AnthropicProviderOptions | GoogleProviderOptions | None | OpenAiProviderOptions | Unset = UNSET
+ compatible_provider_name: str | Unset = UNSET
+ provider_base_url: str | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.anthropic_provider_options import AnthropicProviderOptions
+ from ..models.google_provider_options import GoogleProviderOptions
+ from ..models.open_ai_provider_options import OpenAiProviderOptions
+
+ name = self.name
+
+ description: None | str | Unset
+ if isinstance(self.description, Unset):
+ description = UNSET
+ else:
+ description = self.description
+
+ system_prompt = self.system_prompt
+
+ knowledge_prompt: None | str | Unset
+ if isinstance(self.knowledge_prompt, Unset):
+ knowledge_prompt = UNSET
+ else:
+ knowledge_prompt = self.knowledge_prompt
+
+ always_use_knowledge = self.always_use_knowledge
+
+ settings: dict[str, Any] | None | Unset
+ if isinstance(self.settings, Unset):
+ settings = UNSET
+ elif isinstance(self.settings, AiCopilotProviderSettings):
+ settings = self.settings.to_dict()
+ else:
+ settings = self.settings
+
+ provider_api_key = self.provider_api_key
+
+ provider: str | Unset = UNSET
+ if not isinstance(self.provider, Unset):
+ provider = self.provider.value
+
+ provider_model = self.provider_model
+
+ provider_options: dict[str, Any] | None | Unset
+ if isinstance(self.provider_options, Unset):
+ provider_options = UNSET
+ elif isinstance(self.provider_options, OpenAiProviderOptions):
+ provider_options = self.provider_options.to_dict()
+ elif isinstance(self.provider_options, AnthropicProviderOptions):
+ provider_options = self.provider_options.to_dict()
+ elif isinstance(self.provider_options, GoogleProviderOptions):
+ provider_options = self.provider_options.to_dict()
+ else:
+ provider_options = self.provider_options
+
+ compatible_provider_name = self.compatible_provider_name
+
+ provider_base_url = self.provider_base_url
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update({})
+ if name is not UNSET:
+ field_dict["name"] = name
+ if description is not UNSET:
+ field_dict["description"] = description
+ if system_prompt is not UNSET:
+ field_dict["systemPrompt"] = system_prompt
+ if knowledge_prompt is not UNSET:
+ field_dict["knowledgePrompt"] = knowledge_prompt
+ if always_use_knowledge is not UNSET:
+ field_dict["alwaysUseKnowledge"] = always_use_knowledge
+ if settings is not UNSET:
+ field_dict["settings"] = settings
+ if provider_api_key is not UNSET:
+ field_dict["providerApiKey"] = provider_api_key
+ if provider is not UNSET:
+ field_dict["provider"] = provider
+ if provider_model is not UNSET:
+ field_dict["providerModel"] = provider_model
+ if provider_options is not UNSET:
+ field_dict["providerOptions"] = provider_options
+ if compatible_provider_name is not UNSET:
+ field_dict["compatibleProviderName"] = compatible_provider_name
+ if provider_base_url is not UNSET:
+ field_dict["providerBaseUrl"] = provider_base_url
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.ai_copilot_provider_settings import AiCopilotProviderSettings
+ from ..models.anthropic_provider_options import AnthropicProviderOptions
+ from ..models.google_provider_options import GoogleProviderOptions
+ from ..models.open_ai_provider_options import OpenAiProviderOptions
+
+ d = dict(src_dict)
+ name = d.pop("name", UNSET)
+
+ def _parse_description(data: object) -> None | str | Unset:
+ if data is None:
+ return data
+ if isinstance(data, Unset):
+ return data
+ return cast(None | str | Unset, data)
+
+ description = _parse_description(d.pop("description", UNSET))
+
+ system_prompt = d.pop("systemPrompt", UNSET)
+
+ def _parse_knowledge_prompt(data: object) -> None | str | Unset:
+ if data is None:
+ return data
+ if isinstance(data, Unset):
+ return data
+ return cast(None | str | Unset, data)
+
+ knowledge_prompt = _parse_knowledge_prompt(d.pop("knowledgePrompt", UNSET))
+
+ always_use_knowledge = d.pop("alwaysUseKnowledge", UNSET)
+
+ def _parse_settings(data: object) -> AiCopilotProviderSettings | None | Unset:
+ if data is None:
+ return data
+ if isinstance(data, Unset):
+ return data
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ settings_type_0 = AiCopilotProviderSettings.from_dict(data)
+
+ return settings_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(AiCopilotProviderSettings | None | Unset, data)
+
+ settings = _parse_settings(d.pop("settings", UNSET))
+
+ provider_api_key = d.pop("providerApiKey", UNSET)
+
+ _provider = d.pop("provider", UNSET)
+ provider: UpdateAiCopilotRequestBodyProvider | Unset
+ if isinstance(_provider, Unset):
+ provider = UNSET
+ else:
+ provider = UpdateAiCopilotRequestBodyProvider(_provider)
+
+ provider_model = d.pop("providerModel", UNSET)
+
+ def _parse_provider_options(
+ data: object,
+ ) -> AnthropicProviderOptions | GoogleProviderOptions | None | OpenAiProviderOptions | Unset:
+ if data is None:
+ return data
+ if isinstance(data, Unset):
+ return data
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ provider_options_type_0 = OpenAiProviderOptions.from_dict(data)
+
+ return provider_options_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ provider_options_type_1 = AnthropicProviderOptions.from_dict(data)
+
+ return provider_options_type_1
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ try:
+ if not isinstance(data, dict):
+ raise TypeError()
+ provider_options_type_2 = GoogleProviderOptions.from_dict(data)
+
+ return provider_options_type_2
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(AnthropicProviderOptions | GoogleProviderOptions | None | OpenAiProviderOptions | Unset, data)
+
+ provider_options = _parse_provider_options(d.pop("providerOptions", UNSET))
+
+ compatible_provider_name = d.pop("compatibleProviderName", UNSET)
+
+ provider_base_url = d.pop("providerBaseUrl", UNSET)
+
+ update_ai_copilot_request_body = cls(
+ name=name,
+ description=description,
+ system_prompt=system_prompt,
+ knowledge_prompt=knowledge_prompt,
+ always_use_knowledge=always_use_knowledge,
+ settings=settings,
+ provider_api_key=provider_api_key,
+ provider=provider,
+ provider_model=provider_model,
+ provider_options=provider_options,
+ compatible_provider_name=compatible_provider_name,
+ provider_base_url=provider_base_url,
+ )
+
+ return update_ai_copilot_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/update_ai_copilot_request_body_provider.py b/packages/liveblocks-python/liveblocks/models/update_ai_copilot_request_body_provider.py
new file mode 100644
index 0000000000..d37e920ed1
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_ai_copilot_request_body_provider.py
@@ -0,0 +1,8 @@
+from enum import StrEnum
+
+
+class UpdateAiCopilotRequestBodyProvider(StrEnum):
+ ANTHROPIC = "anthropic"
+ GOOGLE = "google"
+ OPENAI = "openai"
+ OPENAI_COMPATIBLE = "openai-compatible"
diff --git a/packages/liveblocks-python/liveblocks/models/update_management_project_request_body.py b/packages/liveblocks-python/liveblocks/models/update_management_project_request_body.py
new file mode 100644
index 0000000000..12f1dbb476
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_management_project_request_body.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class UpdateManagementProjectRequestBody:
+ """
+ Example:
+ {'name': 'Updated Project Name', 'versionCreationTimeout': 60}
+
+ Attributes:
+ name (str | Unset):
+ version_creation_timeout (bool | int | Unset): False to disable timeout or number of seconds between 30 and 300.
+ """
+
+ name: str | Unset = UNSET
+ version_creation_timeout: bool | int | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ name = self.name
+
+ version_creation_timeout: bool | int | Unset
+ if isinstance(self.version_creation_timeout, Unset):
+ version_creation_timeout = UNSET
+ else:
+ version_creation_timeout = self.version_creation_timeout
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if name is not UNSET:
+ field_dict["name"] = name
+ if version_creation_timeout is not UNSET:
+ field_dict["versionCreationTimeout"] = version_creation_timeout
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ name = d.pop("name", UNSET)
+
+ def _parse_version_creation_timeout(data: object) -> bool | int | Unset:
+ if isinstance(data, Unset):
+ return data
+ return cast(bool | int | Unset, data)
+
+ version_creation_timeout = _parse_version_creation_timeout(d.pop("versionCreationTimeout", UNSET))
+
+ update_management_project_request_body = cls(
+ name=name,
+ version_creation_timeout=version_creation_timeout,
+ )
+
+ update_management_project_request_body.additional_properties = d
+ return update_management_project_request_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/update_management_webhook_request_body.py b/packages/liveblocks-python/liveblocks/models/update_management_webhook_request_body.py
new file mode 100644
index 0000000000..e274364de1
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_management_webhook_request_body.py
@@ -0,0 +1,121 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..models.management_webhook_event import ManagementWebhookEvent
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class UpdateManagementWebhookRequestBody:
+ """
+ Example:
+ {'url': 'https://example.com/webhooks', 'subscribedEvents': ['storageUpdated', 'userEntered'], 'rateLimit': 100,
+ 'disabled': False}
+
+ Attributes:
+ url (str | Unset):
+ subscribed_events (list[ManagementWebhookEvent] | Unset):
+ rate_limit (int | Unset):
+ storage_updated_throttle_seconds (int | Unset):
+ y_doc_updated_throttle_seconds (int | Unset):
+ disabled (bool | Unset):
+ """
+
+ url: str | Unset = UNSET
+ subscribed_events: list[ManagementWebhookEvent] | Unset = UNSET
+ rate_limit: int | Unset = UNSET
+ storage_updated_throttle_seconds: int | Unset = UNSET
+ y_doc_updated_throttle_seconds: int | Unset = UNSET
+ disabled: bool | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ url = self.url
+
+ subscribed_events: list[str] | Unset = UNSET
+ if not isinstance(self.subscribed_events, Unset):
+ subscribed_events = []
+ for subscribed_events_item_data in self.subscribed_events:
+ subscribed_events_item = subscribed_events_item_data.value
+ subscribed_events.append(subscribed_events_item)
+
+ rate_limit = self.rate_limit
+
+ storage_updated_throttle_seconds = self.storage_updated_throttle_seconds
+
+ y_doc_updated_throttle_seconds = self.y_doc_updated_throttle_seconds
+
+ disabled = self.disabled
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if url is not UNSET:
+ field_dict["url"] = url
+ if subscribed_events is not UNSET:
+ field_dict["subscribedEvents"] = subscribed_events
+ if rate_limit is not UNSET:
+ field_dict["rateLimit"] = rate_limit
+ if storage_updated_throttle_seconds is not UNSET:
+ field_dict["storageUpdatedThrottleSeconds"] = storage_updated_throttle_seconds
+ if y_doc_updated_throttle_seconds is not UNSET:
+ field_dict["yDocUpdatedThrottleSeconds"] = y_doc_updated_throttle_seconds
+ if disabled is not UNSET:
+ field_dict["disabled"] = disabled
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ url = d.pop("url", UNSET)
+
+ _subscribed_events = d.pop("subscribedEvents", UNSET)
+ subscribed_events: list[ManagementWebhookEvent] | Unset = UNSET
+ if _subscribed_events is not UNSET:
+ subscribed_events = []
+ for subscribed_events_item_data in _subscribed_events:
+ subscribed_events_item = ManagementWebhookEvent(subscribed_events_item_data)
+
+ subscribed_events.append(subscribed_events_item)
+
+ rate_limit = d.pop("rateLimit", UNSET)
+
+ storage_updated_throttle_seconds = d.pop("storageUpdatedThrottleSeconds", UNSET)
+
+ y_doc_updated_throttle_seconds = d.pop("yDocUpdatedThrottleSeconds", UNSET)
+
+ disabled = d.pop("disabled", UNSET)
+
+ update_management_webhook_request_body = cls(
+ url=url,
+ subscribed_events=subscribed_events,
+ rate_limit=rate_limit,
+ storage_updated_throttle_seconds=storage_updated_throttle_seconds,
+ y_doc_updated_throttle_seconds=y_doc_updated_throttle_seconds,
+ disabled=disabled,
+ )
+
+ update_management_webhook_request_body.additional_properties = d
+ return update_management_webhook_request_body
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/update_notification_settings_request_body.py b/packages/liveblocks-python/liveblocks/models/update_notification_settings_request_body.py
new file mode 100644
index 0000000000..77e7b66700
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_notification_settings_request_body.py
@@ -0,0 +1,108 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.notification_channel_settings import NotificationChannelSettings
+
+
+@_attrs_define
+class UpdateNotificationSettingsRequestBody:
+ """Partial notification settings - all properties are optional
+
+ Example:
+ {'email': {'thread': True, 'textMention': False}, 'slack': {'textMention': False}, 'webPush': {'thread': True}}
+
+ Attributes:
+ email (NotificationChannelSettings | Unset): Example: {'thread': True, 'textMention': False,
+ '$customNotification': True}.
+ slack (NotificationChannelSettings | Unset): Example: {'thread': True, 'textMention': False,
+ '$customNotification': True}.
+ teams (NotificationChannelSettings | Unset): Example: {'thread': True, 'textMention': False,
+ '$customNotification': True}.
+ web_push (NotificationChannelSettings | Unset): Example: {'thread': True, 'textMention': False,
+ '$customNotification': True}.
+ """
+
+ email: NotificationChannelSettings | Unset = UNSET
+ slack: NotificationChannelSettings | Unset = UNSET
+ teams: NotificationChannelSettings | Unset = UNSET
+ web_push: NotificationChannelSettings | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ email: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.email, Unset):
+ email = self.email.to_dict()
+
+ slack: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.slack, Unset):
+ slack = self.slack.to_dict()
+
+ teams: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.teams, Unset):
+ teams = self.teams.to_dict()
+
+ web_push: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.web_push, Unset):
+ web_push = self.web_push.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update({})
+ if email is not UNSET:
+ field_dict["email"] = email
+ if slack is not UNSET:
+ field_dict["slack"] = slack
+ if teams is not UNSET:
+ field_dict["teams"] = teams
+ if web_push is not UNSET:
+ field_dict["webPush"] = web_push
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.notification_channel_settings import NotificationChannelSettings
+
+ d = dict(src_dict)
+ _email = d.pop("email", UNSET)
+ email: NotificationChannelSettings | Unset
+ if isinstance(_email, Unset):
+ email = UNSET
+ else:
+ email = NotificationChannelSettings.from_dict(_email)
+
+ _slack = d.pop("slack", UNSET)
+ slack: NotificationChannelSettings | Unset
+ if isinstance(_slack, Unset):
+ slack = UNSET
+ else:
+ slack = NotificationChannelSettings.from_dict(_slack)
+
+ _teams = d.pop("teams", UNSET)
+ teams: NotificationChannelSettings | Unset
+ if isinstance(_teams, Unset):
+ teams = UNSET
+ else:
+ teams = NotificationChannelSettings.from_dict(_teams)
+
+ _web_push = d.pop("webPush", UNSET)
+ web_push: NotificationChannelSettings | Unset
+ if isinstance(_web_push, Unset):
+ web_push = UNSET
+ else:
+ web_push = NotificationChannelSettings.from_dict(_web_push)
+
+ update_notification_settings_request_body = cls(
+ email=email,
+ slack=slack,
+ teams=teams,
+ web_push=web_push,
+ )
+
+ return update_notification_settings_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/update_room_id_request_body.py b/packages/liveblocks-python/liveblocks/models/update_room_id_request_body.py
new file mode 100644
index 0000000000..006b1b2fa7
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_room_id_request_body.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+
+@_attrs_define
+class UpdateRoomIdRequestBody:
+ """
+ Example:
+ {'newRoomId': 'new-room-id'}
+
+ Attributes:
+ new_room_id (str): The new room ID
+ """
+
+ new_room_id: str
+
+ def to_dict(self) -> dict[str, Any]:
+ new_room_id = self.new_room_id
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "newRoomId": new_room_id,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ new_room_id = d.pop("newRoomId")
+
+ update_room_id_request_body = cls(
+ new_room_id=new_room_id,
+ )
+
+ return update_room_id_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/update_room_request_body.py b/packages/liveblocks-python/liveblocks/models/update_room_request_body.py
new file mode 100644
index 0000000000..e776536519
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_room_request_body.py
@@ -0,0 +1,139 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+from ..models.room_permission_item import RoomPermissionItem
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.update_room_request_body_groups_accesses import UpdateRoomRequestBodyGroupsAccesses
+ from ..models.update_room_request_body_metadata import UpdateRoomRequestBodyMetadata
+ from ..models.update_room_request_body_users_accesses import UpdateRoomRequestBodyUsersAccesses
+
+
+@_attrs_define
+class UpdateRoomRequestBody:
+ """
+ Example:
+ {'defaultAccesses': ['room:write'], 'usersAccesses': {'alice': ['room:write']}, 'groupsAccesses': {'marketing':
+ ['room:write']}, 'metadata': {'color': 'blue'}}
+
+ Attributes:
+ default_accesses (list[RoomPermissionItem] | None | Unset):
+ users_accesses (UpdateRoomRequestBodyUsersAccesses | Unset): A map of user identifiers to permissions list.
+ Setting the value as `null` will clear all users’ accesses. Setting one user identifier as `null` will clear
+ this user’s accesses.
+ groups_accesses (UpdateRoomRequestBodyGroupsAccesses | Unset): A map of group identifiers to permissions list.
+ Setting the value as `null` will clear all groups’ accesses. Setting one group identifier as `null` will clear
+ this group’s accesses.
+ metadata (UpdateRoomRequestBodyMetadata | Unset):
+ """
+
+ default_accesses: list[RoomPermissionItem] | None | Unset = UNSET
+ users_accesses: UpdateRoomRequestBodyUsersAccesses | Unset = UNSET
+ groups_accesses: UpdateRoomRequestBodyGroupsAccesses | Unset = UNSET
+ metadata: UpdateRoomRequestBodyMetadata | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ default_accesses: list[str] | None | Unset
+ if isinstance(self.default_accesses, Unset):
+ default_accesses = UNSET
+ elif isinstance(self.default_accesses, list):
+ default_accesses = []
+ for componentsschemas_room_permission_item_data in self.default_accesses:
+ componentsschemas_room_permission_item = componentsschemas_room_permission_item_data.value
+ default_accesses.append(componentsschemas_room_permission_item)
+
+ else:
+ default_accesses = self.default_accesses
+
+ users_accesses: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.users_accesses, Unset):
+ users_accesses = self.users_accesses.to_dict()
+
+ groups_accesses: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.groups_accesses, Unset):
+ groups_accesses = self.groups_accesses.to_dict()
+
+ metadata: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.metadata, Unset):
+ metadata = self.metadata.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update({})
+ if default_accesses is not UNSET:
+ field_dict["defaultAccesses"] = default_accesses
+ if users_accesses is not UNSET:
+ field_dict["usersAccesses"] = users_accesses
+ if groups_accesses is not UNSET:
+ field_dict["groupsAccesses"] = groups_accesses
+ if metadata is not UNSET:
+ field_dict["metadata"] = metadata
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.update_room_request_body_groups_accesses import UpdateRoomRequestBodyGroupsAccesses
+ from ..models.update_room_request_body_metadata import UpdateRoomRequestBodyMetadata
+ from ..models.update_room_request_body_users_accesses import UpdateRoomRequestBodyUsersAccesses
+
+ d = dict(src_dict)
+
+ def _parse_default_accesses(data: object) -> list[RoomPermissionItem] | None | Unset:
+ if data is None:
+ return data
+ if isinstance(data, Unset):
+ return data
+ try:
+ if not isinstance(data, list):
+ raise TypeError()
+ default_accesses_type_0 = []
+ _default_accesses_type_0 = data
+ for componentsschemas_room_permission_item_data in _default_accesses_type_0:
+ componentsschemas_room_permission_item = RoomPermissionItem(
+ componentsschemas_room_permission_item_data
+ )
+
+ default_accesses_type_0.append(componentsschemas_room_permission_item)
+
+ return default_accesses_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(list[RoomPermissionItem] | None | Unset, data)
+
+ default_accesses = _parse_default_accesses(d.pop("defaultAccesses", UNSET))
+
+ _users_accesses = d.pop("usersAccesses", UNSET)
+ users_accesses: UpdateRoomRequestBodyUsersAccesses | Unset
+ if isinstance(_users_accesses, Unset):
+ users_accesses = UNSET
+ else:
+ users_accesses = UpdateRoomRequestBodyUsersAccesses.from_dict(_users_accesses)
+
+ _groups_accesses = d.pop("groupsAccesses", UNSET)
+ groups_accesses: UpdateRoomRequestBodyGroupsAccesses | Unset
+ if isinstance(_groups_accesses, Unset):
+ groups_accesses = UNSET
+ else:
+ groups_accesses = UpdateRoomRequestBodyGroupsAccesses.from_dict(_groups_accesses)
+
+ _metadata = d.pop("metadata", UNSET)
+ metadata: UpdateRoomRequestBodyMetadata | Unset
+ if isinstance(_metadata, Unset):
+ metadata = UNSET
+ else:
+ metadata = UpdateRoomRequestBodyMetadata.from_dict(_metadata)
+
+ update_room_request_body = cls(
+ default_accesses=default_accesses,
+ users_accesses=users_accesses,
+ groups_accesses=groups_accesses,
+ metadata=metadata,
+ )
+
+ return update_room_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/update_room_request_body_groups_accesses.py b/packages/liveblocks-python/liveblocks/models/update_room_request_body_groups_accesses.py
new file mode 100644
index 0000000000..46fbbd1ca9
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_room_request_body_groups_accesses.py
@@ -0,0 +1,95 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..models.update_room_request_body_groups_accesses_additional_property_type_0_item import (
+ UpdateRoomRequestBodyGroupsAccessesAdditionalPropertyType0Item,
+)
+
+
+@_attrs_define
+class UpdateRoomRequestBodyGroupsAccesses:
+ """A map of group identifiers to permissions list. Setting the value as `null` will clear all groups’ accesses. Setting
+ one group identifier as `null` will clear this group’s accesses.
+
+ """
+
+ additional_properties: dict[str, list[UpdateRoomRequestBodyGroupsAccessesAdditionalPropertyType0Item] | None] = (
+ _attrs_field(init=False, factory=dict)
+ )
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ if isinstance(prop, list):
+ field_dict[prop_name] = []
+ for additional_property_type_0_item_data in prop:
+ additional_property_type_0_item = additional_property_type_0_item_data.value
+ field_dict[prop_name].append(additional_property_type_0_item)
+
+ else:
+ field_dict[prop_name] = prop
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ update_room_request_body_groups_accesses = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+
+ def _parse_additional_property(
+ data: object,
+ ) -> list[UpdateRoomRequestBodyGroupsAccessesAdditionalPropertyType0Item] | None:
+ if data is None:
+ return data
+ try:
+ if not isinstance(data, list):
+ raise TypeError()
+ additional_property_type_0 = []
+ _additional_property_type_0 = data
+ for additional_property_type_0_item_data in _additional_property_type_0:
+ additional_property_type_0_item = (
+ UpdateRoomRequestBodyGroupsAccessesAdditionalPropertyType0Item(
+ additional_property_type_0_item_data
+ )
+ )
+
+ additional_property_type_0.append(additional_property_type_0_item)
+
+ return additional_property_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(list[UpdateRoomRequestBodyGroupsAccessesAdditionalPropertyType0Item] | None, data)
+
+ additional_property = _parse_additional_property(prop_dict)
+
+ additional_properties[prop_name] = additional_property
+
+ update_room_request_body_groups_accesses.additional_properties = additional_properties
+ return update_room_request_body_groups_accesses
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> list[UpdateRoomRequestBodyGroupsAccessesAdditionalPropertyType0Item] | None:
+ return self.additional_properties[key]
+
+ def __setitem__(
+ self, key: str, value: list[UpdateRoomRequestBodyGroupsAccessesAdditionalPropertyType0Item] | None
+ ) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/update_room_request_body_groups_accesses_additional_property_type_0_item.py b/packages/liveblocks-python/liveblocks/models/update_room_request_body_groups_accesses_additional_property_type_0_item.py
new file mode 100644
index 0000000000..ef72e4830c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_room_request_body_groups_accesses_additional_property_type_0_item.py
@@ -0,0 +1,8 @@
+from enum import StrEnum
+
+
+class UpdateRoomRequestBodyGroupsAccessesAdditionalPropertyType0Item(StrEnum):
+ COMMENTSWRITE = "comments:write"
+ ROOMPRESENCEWRITE = "room:presence:write"
+ ROOMREAD = "room:read"
+ ROOMWRITE = "room:write"
diff --git a/packages/liveblocks-python/liveblocks/models/update_room_request_body_metadata.py b/packages/liveblocks-python/liveblocks/models/update_room_request_body_metadata.py
new file mode 100644
index 0000000000..86073bc5dd
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_room_request_body_metadata.py
@@ -0,0 +1,70 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class UpdateRoomRequestBodyMetadata:
+ """ """
+
+ additional_properties: dict[str, list[str] | None | str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ if isinstance(prop, list):
+ field_dict[prop_name] = prop
+
+ else:
+ field_dict[prop_name] = prop
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ update_room_request_body_metadata = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+
+ def _parse_additional_property(data: object) -> list[str] | None | str:
+ if data is None:
+ return data
+ try:
+ if not isinstance(data, list):
+ raise TypeError()
+ additional_property_type_1 = cast(list[str], data)
+
+ return additional_property_type_1
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(list[str] | None | str, data)
+
+ additional_property = _parse_additional_property(prop_dict)
+
+ additional_properties[prop_name] = additional_property
+
+ update_room_request_body_metadata.additional_properties = additional_properties
+ return update_room_request_body_metadata
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> list[str] | None | str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: list[str] | None | str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/update_room_request_body_users_accesses.py b/packages/liveblocks-python/liveblocks/models/update_room_request_body_users_accesses.py
new file mode 100644
index 0000000000..decdefea5f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_room_request_body_users_accesses.py
@@ -0,0 +1,93 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..models.update_room_request_body_users_accesses_additional_property_type_0_item import (
+ UpdateRoomRequestBodyUsersAccessesAdditionalPropertyType0Item,
+)
+
+
+@_attrs_define
+class UpdateRoomRequestBodyUsersAccesses:
+ """A map of user identifiers to permissions list. Setting the value as `null` will clear all users’ accesses. Setting
+ one user identifier as `null` will clear this user’s accesses.
+
+ """
+
+ additional_properties: dict[str, list[UpdateRoomRequestBodyUsersAccessesAdditionalPropertyType0Item] | None] = (
+ _attrs_field(init=False, factory=dict)
+ )
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ for prop_name, prop in self.additional_properties.items():
+ if isinstance(prop, list):
+ field_dict[prop_name] = []
+ for additional_property_type_0_item_data in prop:
+ additional_property_type_0_item = additional_property_type_0_item_data.value
+ field_dict[prop_name].append(additional_property_type_0_item)
+
+ else:
+ field_dict[prop_name] = prop
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ update_room_request_body_users_accesses = cls()
+
+ additional_properties = {}
+ for prop_name, prop_dict in d.items():
+
+ def _parse_additional_property(
+ data: object,
+ ) -> list[UpdateRoomRequestBodyUsersAccessesAdditionalPropertyType0Item] | None:
+ if data is None:
+ return data
+ try:
+ if not isinstance(data, list):
+ raise TypeError()
+ additional_property_type_0 = []
+ _additional_property_type_0 = data
+ for additional_property_type_0_item_data in _additional_property_type_0:
+ additional_property_type_0_item = UpdateRoomRequestBodyUsersAccessesAdditionalPropertyType0Item(
+ additional_property_type_0_item_data
+ )
+
+ additional_property_type_0.append(additional_property_type_0_item)
+
+ return additional_property_type_0
+ except (TypeError, ValueError, AttributeError, KeyError):
+ pass
+ return cast(list[UpdateRoomRequestBodyUsersAccessesAdditionalPropertyType0Item] | None, data)
+
+ additional_property = _parse_additional_property(prop_dict)
+
+ additional_properties[prop_name] = additional_property
+
+ update_room_request_body_users_accesses.additional_properties = additional_properties
+ return update_room_request_body_users_accesses
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> list[UpdateRoomRequestBodyUsersAccessesAdditionalPropertyType0Item] | None:
+ return self.additional_properties[key]
+
+ def __setitem__(
+ self, key: str, value: list[UpdateRoomRequestBodyUsersAccessesAdditionalPropertyType0Item] | None
+ ) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/update_room_request_body_users_accesses_additional_property_type_0_item.py b/packages/liveblocks-python/liveblocks/models/update_room_request_body_users_accesses_additional_property_type_0_item.py
new file mode 100644
index 0000000000..e8e9e86284
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_room_request_body_users_accesses_additional_property_type_0_item.py
@@ -0,0 +1,8 @@
+from enum import StrEnum
+
+
+class UpdateRoomRequestBodyUsersAccessesAdditionalPropertyType0Item(StrEnum):
+ COMMENTSWRITE = "comments:write"
+ ROOMPRESENCEWRITE = "room:presence:write"
+ ROOMREAD = "room:read"
+ ROOMWRITE = "room:write"
diff --git a/packages/liveblocks-python/liveblocks/models/update_room_subscription_settings_request_body.py b/packages/liveblocks-python/liveblocks/models/update_room_subscription_settings_request_body.py
new file mode 100644
index 0000000000..5e7c8616d4
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_room_subscription_settings_request_body.py
@@ -0,0 +1,73 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+from ..models.update_room_subscription_settings_request_body_text_mentions import (
+ UpdateRoomSubscriptionSettingsRequestBodyTextMentions,
+)
+from ..models.update_room_subscription_settings_request_body_threads import (
+ UpdateRoomSubscriptionSettingsRequestBodyThreads,
+)
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class UpdateRoomSubscriptionSettingsRequestBody:
+ """Partial room subscription settings - all properties are optional
+
+ Example:
+ {'threads': 'replies_and_mentions', 'textMentions': 'none'}
+
+ Attributes:
+ threads (UpdateRoomSubscriptionSettingsRequestBodyThreads | Unset):
+ text_mentions (UpdateRoomSubscriptionSettingsRequestBodyTextMentions | Unset):
+ """
+
+ threads: UpdateRoomSubscriptionSettingsRequestBodyThreads | Unset = UNSET
+ text_mentions: UpdateRoomSubscriptionSettingsRequestBodyTextMentions | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ threads: str | Unset = UNSET
+ if not isinstance(self.threads, Unset):
+ threads = self.threads.value
+
+ text_mentions: str | Unset = UNSET
+ if not isinstance(self.text_mentions, Unset):
+ text_mentions = self.text_mentions.value
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update({})
+ if threads is not UNSET:
+ field_dict["threads"] = threads
+ if text_mentions is not UNSET:
+ field_dict["textMentions"] = text_mentions
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ _threads = d.pop("threads", UNSET)
+ threads: UpdateRoomSubscriptionSettingsRequestBodyThreads | Unset
+ if isinstance(_threads, Unset):
+ threads = UNSET
+ else:
+ threads = UpdateRoomSubscriptionSettingsRequestBodyThreads(_threads)
+
+ _text_mentions = d.pop("textMentions", UNSET)
+ text_mentions: UpdateRoomSubscriptionSettingsRequestBodyTextMentions | Unset
+ if isinstance(_text_mentions, Unset):
+ text_mentions = UNSET
+ else:
+ text_mentions = UpdateRoomSubscriptionSettingsRequestBodyTextMentions(_text_mentions)
+
+ update_room_subscription_settings_request_body = cls(
+ threads=threads,
+ text_mentions=text_mentions,
+ )
+
+ return update_room_subscription_settings_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/update_room_subscription_settings_request_body_text_mentions.py b/packages/liveblocks-python/liveblocks/models/update_room_subscription_settings_request_body_text_mentions.py
new file mode 100644
index 0000000000..c6d8577510
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_room_subscription_settings_request_body_text_mentions.py
@@ -0,0 +1,6 @@
+from enum import StrEnum
+
+
+class UpdateRoomSubscriptionSettingsRequestBodyTextMentions(StrEnum):
+ MINE = "mine"
+ NONE = "none"
diff --git a/packages/liveblocks-python/liveblocks/models/update_room_subscription_settings_request_body_threads.py b/packages/liveblocks-python/liveblocks/models/update_room_subscription_settings_request_body_threads.py
new file mode 100644
index 0000000000..a2728eea0f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_room_subscription_settings_request_body_threads.py
@@ -0,0 +1,7 @@
+from enum import StrEnum
+
+
+class UpdateRoomSubscriptionSettingsRequestBodyThreads(StrEnum):
+ ALL = "all"
+ NONE = "none"
+ REPLIES_AND_MENTIONS = "replies_and_mentions"
diff --git a/packages/liveblocks-python/liveblocks/models/upsert_management_webhook_headers_request_body.py b/packages/liveblocks-python/liveblocks/models/upsert_management_webhook_headers_request_body.py
new file mode 100644
index 0000000000..35c7eafb7d
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/upsert_management_webhook_headers_request_body.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.management_webhook_additional_headers import ManagementWebhookAdditionalHeaders
+
+
+@_attrs_define
+class UpsertManagementWebhookHeadersRequestBody:
+ """
+ Example:
+ {'headers': {'X-Custom-Header': 'value'}}
+
+ Attributes:
+ headers (ManagementWebhookAdditionalHeaders): Example: {'X-Custom-Header': 'value'}.
+ """
+
+ headers: ManagementWebhookAdditionalHeaders
+
+ def to_dict(self) -> dict[str, Any]:
+ headers = self.headers.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "headers": headers,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.management_webhook_additional_headers import ManagementWebhookAdditionalHeaders
+
+ d = dict(src_dict)
+ headers = ManagementWebhookAdditionalHeaders.from_dict(d.pop("headers"))
+
+ upsert_management_webhook_headers_request_body = cls(
+ headers=headers,
+ )
+
+ return upsert_management_webhook_headers_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/upsert_management_webhook_headers_response.py b/packages/liveblocks-python/liveblocks/models/upsert_management_webhook_headers_response.py
new file mode 100644
index 0000000000..c9f1244325
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/upsert_management_webhook_headers_response.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.upsert_management_webhook_headers_response_headers import (
+ UpsertManagementWebhookHeadersResponseHeaders,
+ )
+
+
+@_attrs_define
+class UpsertManagementWebhookHeadersResponse:
+ """
+ Example:
+ {'headers': {'X-Custom-Header': 'value'}}
+
+ Attributes:
+ headers (UpsertManagementWebhookHeadersResponseHeaders):
+ """
+
+ headers: UpsertManagementWebhookHeadersResponseHeaders
+
+ def to_dict(self) -> dict[str, Any]:
+ headers = self.headers.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "headers": headers,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.upsert_management_webhook_headers_response_headers import (
+ UpsertManagementWebhookHeadersResponseHeaders,
+ )
+
+ d = dict(src_dict)
+ headers = UpsertManagementWebhookHeadersResponseHeaders.from_dict(d.pop("headers"))
+
+ upsert_management_webhook_headers_response = cls(
+ headers=headers,
+ )
+
+ return upsert_management_webhook_headers_response
diff --git a/packages/liveblocks-python/liveblocks/models/upsert_management_webhook_headers_response_headers.py b/packages/liveblocks-python/liveblocks/models/upsert_management_webhook_headers_response_headers.py
new file mode 100644
index 0000000000..dff4d3e552
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/upsert_management_webhook_headers_response_headers.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class UpsertManagementWebhookHeadersResponseHeaders:
+ """ """
+
+ additional_properties: dict[str, str] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ upsert_management_webhook_headers_response_headers = cls()
+
+ upsert_management_webhook_headers_response_headers.additional_properties = d
+ return upsert_management_webhook_headers_response_headers
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> str:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: str) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/upsert_room_request_body.py b/packages/liveblocks-python/liveblocks/models/upsert_room_request_body.py
new file mode 100644
index 0000000000..ddf99a11c1
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/upsert_room_request_body.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self
+
+from attrs import define as _attrs_define
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.create_room_request_body import CreateRoomRequestBody
+ from ..models.update_room_request_body import UpdateRoomRequestBody
+
+
+@_attrs_define
+class UpsertRoomRequestBody:
+ """
+ Example:
+ {'update': {'usersAccesses': {'alice': ['room:write']}, 'groupsAccesses': {'marketing': ['room:write']},
+ 'metadata': {'color': 'blue'}}, 'create': {'defaultAccesses': ['room:write']}}
+
+ Attributes:
+ update (UpdateRoomRequestBody): Example: {'defaultAccesses': ['room:write'], 'usersAccesses': {'alice':
+ ['room:write']}, 'groupsAccesses': {'marketing': ['room:write']}, 'metadata': {'color': 'blue'}}.
+ create (CreateRoomRequestBody | Unset): Example: {'id': 'my-room-id', 'defaultAccesses': ['room:write'],
+ 'metadata': {'color': 'blue'}, 'usersAccesses': {'alice': ['room:write']}, 'groupsAccesses': {'product':
+ ['room:write']}}.
+ """
+
+ update: UpdateRoomRequestBody
+ create: CreateRoomRequestBody | Unset = UNSET
+
+ def to_dict(self) -> dict[str, Any]:
+ update = self.update.to_dict()
+
+ create: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.create, Unset):
+ create = self.create.to_dict()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "update": update,
+ }
+ )
+ if create is not UNSET:
+ field_dict["create"] = create
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.create_room_request_body import CreateRoomRequestBody
+ from ..models.update_room_request_body import UpdateRoomRequestBody
+
+ d = dict(src_dict)
+ update = UpdateRoomRequestBody.from_dict(d.pop("update"))
+
+ _create = d.pop("create", UNSET)
+ create: CreateRoomRequestBody | Unset
+ if isinstance(_create, Unset):
+ create = UNSET
+ else:
+ create = CreateRoomRequestBody.from_dict(_create)
+
+ upsert_room_request_body = cls(
+ update=update,
+ create=create,
+ )
+
+ return upsert_room_request_body
diff --git a/packages/liveblocks-python/liveblocks/models/user_room_subscription_settings.py b/packages/liveblocks-python/liveblocks/models/user_room_subscription_settings.py
new file mode 100644
index 0000000000..8424d6921c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/user_room_subscription_settings.py
@@ -0,0 +1,62 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+
+from ..models.user_room_subscription_settings_text_mentions import UserRoomSubscriptionSettingsTextMentions
+from ..models.user_room_subscription_settings_threads import UserRoomSubscriptionSettingsThreads
+
+
+@_attrs_define
+class UserRoomSubscriptionSettings:
+ """
+ Example:
+ {'threads': 'all', 'textMentions': 'mine', 'roomId': 'my-room-id'}
+
+ Attributes:
+ threads (UserRoomSubscriptionSettingsThreads):
+ text_mentions (UserRoomSubscriptionSettingsTextMentions):
+ room_id (str):
+ """
+
+ threads: UserRoomSubscriptionSettingsThreads
+ text_mentions: UserRoomSubscriptionSettingsTextMentions
+ room_id: str
+
+ def to_dict(self) -> dict[str, Any]:
+ threads = self.threads.value
+
+ text_mentions = self.text_mentions.value
+
+ room_id = self.room_id
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "threads": threads,
+ "textMentions": text_mentions,
+ "roomId": room_id,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ threads = UserRoomSubscriptionSettingsThreads(d.pop("threads"))
+
+ text_mentions = UserRoomSubscriptionSettingsTextMentions(d.pop("textMentions"))
+
+ room_id = d.pop("roomId")
+
+ user_room_subscription_settings = cls(
+ threads=threads,
+ text_mentions=text_mentions,
+ room_id=room_id,
+ )
+
+ return user_room_subscription_settings
diff --git a/packages/liveblocks-python/liveblocks/models/user_room_subscription_settings_text_mentions.py b/packages/liveblocks-python/liveblocks/models/user_room_subscription_settings_text_mentions.py
new file mode 100644
index 0000000000..ff77b5159a
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/user_room_subscription_settings_text_mentions.py
@@ -0,0 +1,6 @@
+from enum import StrEnum
+
+
+class UserRoomSubscriptionSettingsTextMentions(StrEnum):
+ MINE = "mine"
+ NONE = "none"
diff --git a/packages/liveblocks-python/liveblocks/models/user_room_subscription_settings_threads.py b/packages/liveblocks-python/liveblocks/models/user_room_subscription_settings_threads.py
new file mode 100644
index 0000000000..1fd9bb7d6b
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/user_room_subscription_settings_threads.py
@@ -0,0 +1,7 @@
+from enum import StrEnum
+
+
+class UserRoomSubscriptionSettingsThreads(StrEnum):
+ ALL = "all"
+ NONE = "none"
+ REPLIES_AND_MENTIONS = "replies_and_mentions"
diff --git a/packages/liveblocks-python/liveblocks/models/user_subscription.py b/packages/liveblocks-python/liveblocks/models/user_subscription.py
new file mode 100644
index 0000000000..3513567668
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/user_subscription.py
@@ -0,0 +1,91 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class UserSubscription:
+ """
+ Example:
+ {'kind': 'thread', 'subjectId': 'th_abc123', 'createdAt': '2022-07-13T14:32:50.697Z', 'userId': 'alice'}
+
+ Attributes:
+ kind (str):
+ subject_id (str):
+ created_at (datetime.datetime):
+ user_id (str | Unset):
+ """
+
+ kind: str
+ subject_id: str
+ created_at: datetime.datetime
+ user_id: str | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ kind = self.kind
+
+ subject_id = self.subject_id
+
+ created_at = self.created_at.isoformat()
+
+ user_id = self.user_id
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "kind": kind,
+ "subjectId": subject_id,
+ "createdAt": created_at,
+ }
+ )
+ if user_id is not UNSET:
+ field_dict["userId"] = user_id
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ kind = d.pop("kind")
+
+ subject_id = d.pop("subjectId")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ user_id = d.pop("userId", UNSET)
+
+ user_subscription = cls(
+ kind=kind,
+ subject_id=subject_id,
+ created_at=created_at,
+ user_id=user_id,
+ )
+
+ user_subscription.additional_properties = d
+ return user_subscription
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/web_knowledge_source_link.py b/packages/liveblocks-python/liveblocks/models/web_knowledge_source_link.py
new file mode 100644
index 0000000000..7a6fe997ca
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/web_knowledge_source_link.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from dateutil.parser import isoparse
+
+from ..models.web_knowledge_source_link_status import WebKnowledgeSourceLinkStatus
+
+
+@_attrs_define
+class WebKnowledgeSourceLink:
+ """
+ Example:
+ {'id': 'ksl_abc123', 'url': 'https://docs.example.com/getting-started', 'status': 'ready', 'createdAt':
+ '2024-06-01T12:00:00.000Z', 'lastIndexedAt': '2024-06-01T12:00:00.000Z'}
+
+ Attributes:
+ id (str):
+ url (str):
+ status (WebKnowledgeSourceLinkStatus):
+ created_at (datetime.datetime):
+ last_indexed_at (datetime.datetime):
+ """
+
+ id: str
+ url: str
+ status: WebKnowledgeSourceLinkStatus
+ created_at: datetime.datetime
+ last_indexed_at: datetime.datetime
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ url = self.url
+
+ status = self.status.value
+
+ created_at = self.created_at.isoformat()
+
+ last_indexed_at = self.last_indexed_at.isoformat()
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "id": id,
+ "url": url,
+ "status": status,
+ "createdAt": created_at,
+ "lastIndexedAt": last_indexed_at,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ url = d.pop("url")
+
+ status = WebKnowledgeSourceLinkStatus(d.pop("status"))
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ last_indexed_at = isoparse(d.pop("lastIndexedAt"))
+
+ web_knowledge_source_link = cls(
+ id=id,
+ url=url,
+ status=status,
+ created_at=created_at,
+ last_indexed_at=last_indexed_at,
+ )
+
+ return web_knowledge_source_link
diff --git a/packages/liveblocks-python/liveblocks/models/web_knowledge_source_link_status.py b/packages/liveblocks-python/liveblocks/models/web_knowledge_source_link_status.py
new file mode 100644
index 0000000000..f0bc191df3
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/web_knowledge_source_link_status.py
@@ -0,0 +1,7 @@
+from enum import StrEnum
+
+
+class WebKnowledgeSourceLinkStatus(StrEnum):
+ ERROR = "error"
+ INGESTING = "ingesting"
+ READY = "ready"
diff --git a/packages/liveblocks-python/liveblocks/models/yjs_version.py b/packages/liveblocks-python/liveblocks/models/yjs_version.py
new file mode 100644
index 0000000000..c8450bee7c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/yjs_version.py
@@ -0,0 +1,121 @@
+from __future__ import annotations
+
+import datetime
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Literal, Self, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+from dateutil.parser import isoparse
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.yjs_version_authors_item import YjsVersionAuthorsItem
+
+
+@_attrs_define
+class YjsVersion:
+ """
+ Example:
+ {'id': 'vh_abc123', 'type': 'historyVersion', 'createdAt': '2024-10-15T10:30:00.000Z', 'authors': [{'id':
+ 'user-123'}, {'id': 'user-456'}], 'kind': 'yjs'}
+
+ Attributes:
+ id (str): Unique identifier for the version
+ type_ (Literal['historyVersion']):
+ created_at (datetime.datetime): ISO 8601 timestamp of when the version was created
+ kind (Literal['yjs']):
+ authors (list[YjsVersionAuthorsItem] | Unset): List of users who contributed to this version
+ """
+
+ id: str
+ type_: Literal["historyVersion"]
+ created_at: datetime.datetime
+ kind: Literal["yjs"]
+ authors: list[YjsVersionAuthorsItem] | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ type_ = self.type_
+
+ created_at = self.created_at.isoformat()
+
+ kind = self.kind
+
+ authors: list[dict[str, Any]] | Unset = UNSET
+ if not isinstance(self.authors, Unset):
+ authors = []
+ for authors_item_data in self.authors:
+ authors_item = authors_item_data.to_dict()
+ authors.append(authors_item)
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "id": id,
+ "type": type_,
+ "createdAt": created_at,
+ "kind": kind,
+ }
+ )
+ if authors is not UNSET:
+ field_dict["authors"] = authors
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.yjs_version_authors_item import YjsVersionAuthorsItem
+
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ type_ = cast(Literal["historyVersion"], d.pop("type"))
+ if type_ != "historyVersion":
+ raise ValueError(f"type must match const 'historyVersion', got '{type_}'")
+
+ created_at = isoparse(d.pop("createdAt"))
+
+ kind = cast(Literal["yjs"], d.pop("kind"))
+ if kind != "yjs":
+ raise ValueError(f"kind must match const 'yjs', got '{kind}'")
+
+ _authors = d.pop("authors", UNSET)
+ authors: list[YjsVersionAuthorsItem] | Unset = UNSET
+ if _authors is not UNSET:
+ authors = []
+ for authors_item_data in _authors:
+ authors_item = YjsVersionAuthorsItem.from_dict(authors_item_data)
+
+ authors.append(authors_item)
+
+ yjs_version = cls(
+ id=id,
+ type_=type_,
+ created_at=created_at,
+ kind=kind,
+ authors=authors,
+ )
+
+ yjs_version.additional_properties = d
+ return yjs_version
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/models/yjs_version_authors_item.py b/packages/liveblocks-python/liveblocks/models/yjs_version_authors_item.py
new file mode 100644
index 0000000000..83d7e28c08
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/yjs_version_authors_item.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+
+@_attrs_define
+class YjsVersionAuthorsItem:
+ """
+ Attributes:
+ id (str | Unset): User ID of the author
+ """
+
+ id: str | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ id = self.id
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if id is not UNSET:
+ field_dict["id"] = id
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ id = d.pop("id", UNSET)
+
+ yjs_version_authors_item = cls(
+ id=id,
+ )
+
+ yjs_version_authors_item.additional_properties = d
+ return yjs_version_authors_item
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/packages/liveblocks-python/liveblocks/py.typed b/packages/liveblocks-python/liveblocks/py.typed
new file mode 100644
index 0000000000..1aad32711f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/py.typed
@@ -0,0 +1 @@
+# Marker file for PEP 561
\ No newline at end of file
diff --git a/packages/liveblocks-python/liveblocks/session.py b/packages/liveblocks-python/liveblocks/session.py
new file mode 100644
index 0000000000..d06714122c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/session.py
@@ -0,0 +1,189 @@
+from __future__ import annotations
+
+import re
+import warnings
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any, Literal
+
+if TYPE_CHECKING:
+ from liveblocks.client import AsyncLiveblocks, Liveblocks
+ from liveblocks.models.authorize_user_request_body import AuthorizeUserRequestBody
+ from liveblocks.models.authorize_user_response import AuthorizeUserResponse
+
+Permission = Literal[
+ "room:write",
+ "room:read",
+ "room:presence:write",
+ "comments:write",
+ "comments:read",
+]
+
+ALL_PERMISSIONS: frozenset[str] = frozenset(
+ ["room:write", "room:read", "room:presence:write", "comments:write", "comments:read"]
+)
+
+READ_ACCESS: tuple[Permission, ...] = ("room:read", "room:presence:write", "comments:read")
+FULL_ACCESS: tuple[Permission, ...] = ("room:write", "comments:write")
+
+_MAX_PERMS_PER_SET = 10
+_ROOM_PATTERN_RE = re.compile(r"^([*]|[^*]{1,128}[*]?)$")
+
+
+class _BaseSession:
+ """Shared permission-building logic for sync and async sessions."""
+
+ FULL_ACCESS = FULL_ACCESS
+ READ_ACCESS = READ_ACCESS
+
+ def __init__(
+ self,
+ user_id: str,
+ user_info: dict[str, Any] | None = None,
+ organization_id: str | None = None,
+ ) -> None:
+ if not user_id:
+ raise ValueError(
+ "Invalid value for 'user_id'. Please provide a non-empty string. "
+ "For more information: https://liveblocks.io/docs/api-reference/liveblocks-node#authorize"
+ )
+
+ self._user_id = user_id
+ self._user_info = user_info
+ self._organization_id = organization_id
+ self._sealed = False
+ self._permissions: dict[str, list[str]] = {}
+
+ def _get_or_create(self, room_id: str) -> list[str]:
+ if self._sealed:
+ raise RuntimeError("You can no longer change these permissions.")
+
+ perms = self._permissions.get(room_id)
+ if perms is not None:
+ return perms
+
+ if len(self._permissions) >= _MAX_PERMS_PER_SET:
+ raise RuntimeError("You cannot add permissions for more than 10 rooms in a single token")
+
+ perms = []
+ self._permissions[room_id] = perms
+ return perms
+
+ def allow(self, room_id_or_pattern: str, permissions: Sequence[Permission]) -> _BaseSession:
+ if not _ROOM_PATTERN_RE.match(room_id_or_pattern):
+ raise ValueError("Invalid room name or pattern")
+ if not permissions:
+ raise ValueError("Permission list cannot be empty")
+
+ existing = self._get_or_create(room_id_or_pattern)
+ for perm in permissions:
+ if perm not in ALL_PERMISSIONS:
+ raise ValueError(f"Not a valid permission: {perm}")
+ if perm not in existing:
+ existing.append(perm)
+ return self
+
+ @property
+ def _has_permissions(self) -> bool:
+ return bool(self._permissions)
+
+ def _serialize_permissions(self) -> dict[str, list[str]]:
+ return {pat: list(perms) for pat, perms in self._permissions.items()}
+
+ def _build_request_body(self) -> AuthorizeUserRequestBody:
+ from liveblocks.models.authorize_user_request_body import AuthorizeUserRequestBody
+ from liveblocks.models.authorize_user_request_body_permissions import (
+ AuthorizeUserRequestBodyPermissions,
+ )
+ from liveblocks.models.authorize_user_request_body_user_info import (
+ AuthorizeUserRequestBodyUserInfo,
+ )
+
+ if self._sealed:
+ raise RuntimeError("You cannot reuse Session instances. Please create a new session every time.")
+ self._sealed = True
+ if not self._permissions:
+ warnings.warn(
+ "Access tokens without any permission will not be supported soon, "
+ "you should use wildcards when the client requests a token for "
+ "resources outside a room. See https://liveblocks.io/docs/errors/"
+ "liveblocks-client/access-tokens-not-enough-permissions",
+ stacklevel=3,
+ )
+
+ perms_model = AuthorizeUserRequestBodyPermissions()
+ perms_model.additional_properties = self._serialize_permissions()
+
+ user_info_model: AuthorizeUserRequestBodyUserInfo | None = None
+ if self._user_info is not None:
+ user_info_model = AuthorizeUserRequestBodyUserInfo()
+ user_info_model.additional_properties = dict(self._user_info)
+
+ body = AuthorizeUserRequestBody(
+ user_id=self._user_id,
+ permissions=perms_model,
+ )
+
+ if user_info_model is not None:
+ body.user_info = user_info_model
+ if self._organization_id is not None:
+ body.organization_id = self._organization_id
+
+ return body
+
+
+class Session(_BaseSession):
+ """Synchronous session. Created by ``Liveblocks.prepare_session()``.
+
+ Usage::
+
+ session = client.prepare_session("user-123")
+ session.allow("my-room", session.FULL_ACCESS)
+ result = session.authorize()
+ """
+
+ def __init__(
+ self,
+ client: Liveblocks,
+ user_id: str,
+ user_info: dict[str, Any] | None = None,
+ organization_id: str | None = None,
+ ) -> None:
+ super().__init__(user_id, user_info=user_info, organization_id=organization_id)
+ self._client = client
+
+ def allow(self, room_id_or_pattern: str, permissions: Sequence[Permission]) -> Session:
+ super().allow(room_id_or_pattern, permissions)
+ return self
+
+ def authorize(self) -> AuthorizeUserResponse:
+ body = self._build_request_body()
+ return self._client.authorize_user(body=body)
+
+
+class AsyncSession(_BaseSession):
+ """Asynchronous session. Created by ``AsyncLiveblocks.prepare_session()``.
+
+ Usage::
+
+ session = client.prepare_session("user-123")
+ session.allow("my-room", session.FULL_ACCESS)
+ result = await session.authorize()
+ """
+
+ def __init__(
+ self,
+ client: AsyncLiveblocks,
+ user_id: str,
+ user_info: dict[str, Any] | None = None,
+ organization_id: str | None = None,
+ ) -> None:
+ super().__init__(user_id, user_info=user_info, organization_id=organization_id)
+ self._client = client
+
+ def allow(self, room_id_or_pattern: str, permissions: Sequence[Permission]) -> AsyncSession:
+ super().allow(room_id_or_pattern, permissions)
+ return self
+
+ async def authorize(self) -> AuthorizeUserResponse:
+ body = self._build_request_body()
+ return await self._client.authorize_user(body=body)
diff --git a/packages/liveblocks-python/liveblocks/types.py b/packages/liveblocks-python/liveblocks/types.py
new file mode 100644
index 0000000000..6e2176cd4e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/types.py
@@ -0,0 +1,39 @@
+"""Contains some shared types for properties"""
+
+from collections.abc import Mapping
+from typing import IO, BinaryIO, Literal
+
+from attrs import define
+
+
+class Unset:
+ def __bool__(self) -> Literal[False]:
+ return False
+
+
+UNSET: Unset = Unset()
+
+# The types that `httpx.Client(files=)` can accept, copied from that library.
+FileContent = IO[bytes] | bytes | str
+FileTypes = (
+ # (filename, file (or bytes), content_type)
+ tuple[str | None, FileContent, str | None]
+ # (filename, file (or bytes), content_type, headers)
+ | tuple[str | None, FileContent, str | None, Mapping[str, str]]
+)
+
+
+@define
+class File:
+ """Contains information for file uploads"""
+
+ payload: BinaryIO
+ file_name: str | None = None
+ mime_type: str | None = None
+
+ def to_tuple(self) -> FileTypes:
+ """Return a tuple representation that httpx will accept for multipart/form-data"""
+ return self.file_name, self.payload, self.mime_type
+
+
+__all__ = ["UNSET", "File", "FileTypes", "Unset"]
diff --git a/packages/liveblocks-python/liveblocks/webhooks.py b/packages/liveblocks-python/liveblocks/webhooks.py
new file mode 100644
index 0000000000..acee5b9a84
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/webhooks.py
@@ -0,0 +1,525 @@
+from __future__ import annotations
+
+import base64
+import hashlib
+import hmac
+import json
+import time
+from typing import Any, Literal, TypedDict, TypeGuard
+
+_SECRET_PREFIX = "whsec_"
+_TOLERANCE_IN_SECONDS = 5 * 60 # 5 minutes
+
+_KNOWN_EVENT_TYPES = frozenset(
+ {
+ "storageUpdated",
+ "userEntered",
+ "userLeft",
+ "roomCreated",
+ "roomDeleted",
+ "commentCreated",
+ "commentEdited",
+ "commentDeleted",
+ "commentReactionAdded",
+ "commentReactionRemoved",
+ "commentMetadataUpdated",
+ "threadMetadataUpdated",
+ "threadCreated",
+ "threadDeleted",
+ "ydocUpdated",
+ "notification",
+ "threadMarkedAsResolved",
+ "threadMarkedAsUnresolved",
+ }
+)
+
+NotificationChannel = Literal["email", "slack", "teams", "webPush"]
+
+__all__ = [
+ "WebhookHandler",
+ "WebhookEvent",
+ "NotificationEvent",
+ "NotificationChannel",
+ "StorageUpdatedEvent",
+ "UserEnteredEvent",
+ "UserLeftEvent",
+ "RoomCreatedEvent",
+ "RoomDeletedEvent",
+ "CommentCreatedEvent",
+ "CommentEditedEvent",
+ "CommentDeletedEvent",
+ "CommentReactionAddedEvent",
+ "CommentReactionRemovedEvent",
+ "CommentMetadataUpdatedEvent",
+ "ThreadMetadataUpdatedEvent",
+ "ThreadCreatedEvent",
+ "ThreadDeletedEvent",
+ "ThreadMarkedAsResolvedEvent",
+ "ThreadMarkedAsUnresolvedEvent",
+ "YDocUpdatedEvent",
+ "ThreadNotificationEvent",
+ "TextMentionNotificationEvent",
+ "CustomNotificationEvent",
+ "is_thread_notification_event",
+ "is_text_mention_notification_event",
+ "is_custom_notification_event",
+]
+
+# ---------------------------------------------------------------------------
+# Event data TypedDicts
+# ---------------------------------------------------------------------------
+
+
+class _StorageUpdatedData(TypedDict):
+ roomId: str
+ projectId: str
+ updatedAt: str # ISO 8601
+
+
+class _UserEnteredData(TypedDict):
+ projectId: str
+ roomId: str
+ connectionId: int
+ userId: str | None
+ userInfo: dict[str, Any] | None
+ enteredAt: str # ISO 8601
+ numActiveUsers: int
+
+
+class _UserLeftData(TypedDict):
+ projectId: str
+ roomId: str
+ connectionId: int
+ userId: str | None
+ userInfo: dict[str, Any] | None
+ leftAt: str # ISO 8601
+ numActiveUsers: int
+
+
+class _RoomCreatedData(TypedDict):
+ projectId: str
+ roomId: str
+ createdAt: str # ISO 8601
+
+
+class _RoomDeletedData(TypedDict):
+ projectId: str
+ roomId: str
+ deletedAt: str # ISO 8601
+
+
+class _CommentCreatedData(TypedDict):
+ projectId: str
+ roomId: str
+ threadId: str
+ commentId: str
+ createdAt: str # ISO 8601
+ createdBy: str
+
+
+class _CommentEditedData(TypedDict):
+ projectId: str
+ roomId: str
+ threadId: str
+ commentId: str
+ editedAt: str # ISO 8601
+
+
+class _CommentDeletedData(TypedDict):
+ projectId: str
+ roomId: str
+ threadId: str
+ commentId: str
+ deletedAt: str # ISO 8601
+
+
+class _CommentReactionAddedData(TypedDict):
+ projectId: str
+ roomId: str
+ threadId: str
+ commentId: str
+ emoji: str
+ addedAt: str # ISO 8601
+ addedBy: str
+
+
+class _CommentReactionRemovedData(TypedDict):
+ projectId: str
+ roomId: str
+ threadId: str
+ commentId: str
+ emoji: str
+ removedAt: str # ISO 8601
+ removedBy: str
+
+
+class _CommentMetadataUpdatedData(TypedDict):
+ projectId: str
+ roomId: str
+ threadId: str
+ commentId: str
+ updatedAt: str # ISO 8601
+ updatedBy: str
+
+
+class _ThreadMetadataUpdatedData(TypedDict):
+ projectId: str
+ roomId: str
+ threadId: str
+ updatedAt: str # ISO 8601
+ updatedBy: str
+
+
+class _ThreadCreatedData(TypedDict):
+ projectId: str
+ roomId: str
+ threadId: str
+ createdAt: str # ISO 8601
+ createdBy: str
+
+
+class _ThreadDeletedData(TypedDict):
+ projectId: str
+ roomId: str
+ threadId: str
+ deletedAt: str # ISO 8601
+
+
+class _ThreadMarkedAsResolvedData(TypedDict):
+ projectId: str
+ roomId: str
+ threadId: str
+ updatedAt: str # ISO 8601
+ updatedBy: str
+
+
+class _ThreadMarkedAsUnresolvedData(TypedDict):
+ projectId: str
+ roomId: str
+ threadId: str
+ updatedAt: str # ISO 8601
+ updatedBy: str
+
+
+class _YDocUpdatedData(TypedDict):
+ projectId: str
+ roomId: str
+ updatedAt: str # ISO 8601
+
+
+class _ThreadNotificationData(TypedDict):
+ channel: NotificationChannel
+ kind: Literal["thread"]
+ projectId: str
+ roomId: str
+ userId: str
+ threadId: str
+ inboxNotificationId: str
+ createdAt: str # ISO 8601
+ triggeredAt: str # ISO 8601
+
+
+class _TextMentionNotificationData(TypedDict):
+ channel: NotificationChannel
+ kind: Literal["textMention"]
+ projectId: str
+ roomId: str
+ userId: str
+ mentionId: str
+ inboxNotificationId: str
+ createdAt: str # ISO 8601
+ triggeredAt: str # ISO 8601
+
+
+class _CustomNotificationData(TypedDict):
+ channel: NotificationChannel
+ kind: str # starts with "$"
+ projectId: str
+ roomId: str | None
+ userId: str
+ subjectId: str
+ inboxNotificationId: str
+ createdAt: str # ISO 8601
+ triggeredAt: str # ISO 8601
+
+
+# ---------------------------------------------------------------------------
+# Event TypedDicts
+# ---------------------------------------------------------------------------
+
+
+class StorageUpdatedEvent(TypedDict):
+ type: Literal["storageUpdated"]
+ data: _StorageUpdatedData
+
+
+class UserEnteredEvent(TypedDict):
+ type: Literal["userEntered"]
+ data: _UserEnteredData
+
+
+class UserLeftEvent(TypedDict):
+ type: Literal["userLeft"]
+ data: _UserLeftData
+
+
+class RoomCreatedEvent(TypedDict):
+ type: Literal["roomCreated"]
+ data: _RoomCreatedData
+
+
+class RoomDeletedEvent(TypedDict):
+ type: Literal["roomDeleted"]
+ data: _RoomDeletedData
+
+
+class CommentCreatedEvent(TypedDict):
+ type: Literal["commentCreated"]
+ data: _CommentCreatedData
+
+
+class CommentEditedEvent(TypedDict):
+ type: Literal["commentEdited"]
+ data: _CommentEditedData
+
+
+class CommentDeletedEvent(TypedDict):
+ type: Literal["commentDeleted"]
+ data: _CommentDeletedData
+
+
+class CommentReactionAddedEvent(TypedDict):
+ type: Literal["commentReactionAdded"]
+ data: _CommentReactionAddedData
+
+
+class CommentReactionRemovedEvent(TypedDict):
+ type: Literal["commentReactionRemoved"]
+ data: _CommentReactionRemovedData
+
+
+class CommentMetadataUpdatedEvent(TypedDict):
+ type: Literal["commentMetadataUpdated"]
+ data: _CommentMetadataUpdatedData
+
+
+class ThreadMetadataUpdatedEvent(TypedDict):
+ type: Literal["threadMetadataUpdated"]
+ data: _ThreadMetadataUpdatedData
+
+
+class ThreadCreatedEvent(TypedDict):
+ type: Literal["threadCreated"]
+ data: _ThreadCreatedData
+
+
+class ThreadDeletedEvent(TypedDict):
+ type: Literal["threadDeleted"]
+ data: _ThreadDeletedData
+
+
+class ThreadMarkedAsResolvedEvent(TypedDict):
+ type: Literal["threadMarkedAsResolved"]
+ data: _ThreadMarkedAsResolvedData
+
+
+class ThreadMarkedAsUnresolvedEvent(TypedDict):
+ type: Literal["threadMarkedAsUnresolved"]
+ data: _ThreadMarkedAsUnresolvedData
+
+
+class YDocUpdatedEvent(TypedDict):
+ type: Literal["ydocUpdated"]
+ data: _YDocUpdatedData
+
+
+class ThreadNotificationEvent(TypedDict):
+ type: Literal["notification"]
+ data: _ThreadNotificationData
+
+
+class TextMentionNotificationEvent(TypedDict):
+ type: Literal["notification"]
+ data: _TextMentionNotificationData
+
+
+class CustomNotificationEvent(TypedDict):
+ type: Literal["notification"]
+ data: _CustomNotificationData
+
+
+NotificationEvent = ThreadNotificationEvent | TextMentionNotificationEvent | CustomNotificationEvent
+
+WebhookEvent = (
+ StorageUpdatedEvent
+ | UserEnteredEvent
+ | UserLeftEvent
+ | RoomCreatedEvent
+ | RoomDeletedEvent
+ | CommentCreatedEvent
+ | CommentEditedEvent
+ | CommentDeletedEvent
+ | CommentReactionAddedEvent
+ | CommentReactionRemovedEvent
+ | CommentMetadataUpdatedEvent
+ | ThreadMetadataUpdatedEvent
+ | ThreadCreatedEvent
+ | ThreadDeletedEvent
+ | ThreadMarkedAsResolvedEvent
+ | ThreadMarkedAsUnresolvedEvent
+ | YDocUpdatedEvent
+ | ThreadNotificationEvent
+ | TextMentionNotificationEvent
+ | CustomNotificationEvent
+)
+
+
+# ---------------------------------------------------------------------------
+# Type guards
+# ---------------------------------------------------------------------------
+
+
+def _is_custom_kind(value: object) -> bool:
+ return isinstance(value, str) and value.startswith("$")
+
+
+def is_thread_notification_event(event: WebhookEvent) -> TypeGuard[ThreadNotificationEvent]:
+ """Check whether *event* is a thread notification event."""
+ return event["type"] == "notification" and event["data"].get("kind") == "thread"
+
+
+def is_text_mention_notification_event(event: WebhookEvent) -> TypeGuard[TextMentionNotificationEvent]:
+ """Check whether *event* is a text-mention notification event."""
+ return event["type"] == "notification" and event["data"].get("kind") == "textMention"
+
+
+def is_custom_notification_event(event: WebhookEvent) -> TypeGuard[CustomNotificationEvent]:
+ """Check whether *event* is a custom notification event."""
+ return event["type"] == "notification" and _is_custom_kind(event["data"].get("kind"))
+
+
+# ---------------------------------------------------------------------------
+# WebhookHandler
+# ---------------------------------------------------------------------------
+
+
+class WebhookHandler:
+ """Verify incoming Liveblocks webhook requests.
+
+ Usage::
+
+ handler = WebhookHandler("whsec_...")
+ event = handler.verify_request(headers=request.headers, raw_body=request.body)
+ """
+
+ def __init__(self, secret: str) -> None:
+ if not secret or not isinstance(secret, str):
+ raise ValueError("Secret is required and must be a non-empty string")
+
+ if not secret.startswith(_SECRET_PREFIX):
+ raise ValueError("Invalid secret, must start with whsec_")
+
+ secret_key = secret[len(_SECRET_PREFIX) :]
+ try:
+ self._secret_bytes = base64.b64decode(secret_key)
+ except Exception:
+ raise ValueError(
+ "Webhook secret contains invalid base64 after the 'whsec_' prefix. "
+ "Please copy the full secret from your Liveblocks dashboard."
+ ) from None
+
+ def verify_request(self, *, headers: dict[str, str], raw_body: str) -> WebhookEvent:
+ """Verify a webhook request and return the parsed event.
+
+ Args:
+ headers: The HTTP headers as a string-to-string mapping.
+ raw_body: The raw request body as a string (do **not** parse it first).
+
+ Returns:
+ The parsed webhook event dictionary.
+
+ Raises:
+ ValueError: If the request cannot be verified.
+ """
+ webhook_id, timestamp, raw_signatures = self._verify_headers(headers)
+
+ if not isinstance(raw_body, str):
+ raise ValueError(
+ f"Invalid raw_body, must be a string, got {type(raw_body).__name__!r} instead. "
+ "Make sure you pass the raw request body string, not a parsed object."
+ )
+
+ self._verify_timestamp(timestamp)
+
+ signature = self._sign(f"{webhook_id}.{timestamp}.{raw_body}")
+
+ expected_signatures = [
+ parts[1] for raw_sig in raw_signatures.split(" ") if len(parts := raw_sig.split(",")) > 1
+ ]
+
+ if not any(hmac.compare_digest(signature, s) for s in expected_signatures):
+ raise ValueError(
+ f"Invalid signature for webhook {webhook_id}. "
+ "Make sure you are using the correct webhook secret "
+ "and that the raw request body is passed unchanged "
+ "(not parsed or re-serialized)."
+ )
+
+ event: WebhookEvent = json.loads(raw_body)
+
+ self._verify_event_type(event)
+
+ return event
+
+ # -- private helpers ----------------------------------------------------
+
+ @staticmethod
+ def _verify_headers(headers: dict[str, str]) -> tuple[str, str, str]:
+ normalized = {k.lower(): v for k, v in headers.items()}
+
+ webhook_id = normalized.get("webhook-id")
+ if not isinstance(webhook_id, str):
+ raise ValueError("Invalid webhook-id header")
+
+ timestamp = normalized.get("webhook-timestamp")
+ if not isinstance(timestamp, str):
+ raise ValueError("Invalid webhook-timestamp header")
+
+ raw_signatures = normalized.get("webhook-signature")
+ if not isinstance(raw_signatures, str):
+ raise ValueError("Invalid webhook-signature header")
+
+ return webhook_id, timestamp, raw_signatures
+
+ def _sign(self, content: str) -> str:
+ mac = hmac.new(self._secret_bytes, content.encode(), hashlib.sha256)
+ return base64.b64encode(mac.digest()).decode()
+
+ @staticmethod
+ def _verify_timestamp(timestamp_header: str) -> None:
+ try:
+ timestamp = int(timestamp_header)
+ except ValueError:
+ raise ValueError("Invalid timestamp") from None
+
+ now = int(time.time())
+
+ if timestamp < now - _TOLERANCE_IN_SECONDS:
+ raise ValueError("Timestamp too old")
+
+ if timestamp > now + _TOLERANCE_IN_SECONDS:
+ raise ValueError("Timestamp in the future")
+
+ @staticmethod
+ def _verify_event_type(event: WebhookEvent) -> None:
+ event_type = event.get("type") if isinstance(event, dict) else None
+
+ if event_type and event_type in _KNOWN_EVENT_TYPES:
+ if event_type == "notification":
+ kind = event["data"].get("kind")
+ if kind in ("thread", "textMention") or _is_custom_kind(kind):
+ return
+ raise ValueError(f"Unknown notification kind: {kind!r}")
+ return
+
+ raise ValueError("Unknown event type, please upgrade to a higher version of liveblocks")
diff --git a/packages/liveblocks-python/pyproject.toml b/packages/liveblocks-python/pyproject.toml
new file mode 100644
index 0000000000..42f7c31311
--- /dev/null
+++ b/packages/liveblocks-python/pyproject.toml
@@ -0,0 +1,61 @@
+[project]
+name = "liveblocks"
+version = "0.1.0"
+description = "A client library for accessing Liveblocks API"
+authors = []
+requires-python = ">=3.11"
+readme = "README.md"
+dependencies = [
+ "httpx>=0.23.0,<0.29.0",
+ "attrs>=22.2.0",
+ "python-dateutil>=2.8.0,<3",
+]
+
+[dependency-groups]
+dev = [
+ "pytest>=9.0",
+ "pytest-cov>=7.0",
+ "respx>=0.22.0",
+]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+
+[tool.coverage.run]
+source = ["liveblocks"]
+omit = [
+ "liveblocks/models/*",
+ "liveblocks/api/*",
+]
+
+[tool.coverage.report]
+show_missing = true
+fail_under = 0
+# Exclude the delegation boilerplate inside client.py methods.
+# Each public method on Liveblocks/AsyncLiveblocks just imports an
+# API module and forwards the call — the real logic lives in
+# liveblocks/api/* (already omitted above). These patterns skip:
+# 1. The local import line (e.g. `from .api.room import get_rooms`)
+# 2. The sync return line (e.g. `return get_rooms._sync(...)`)
+# 3. The async return line (e.g. `return await get_rooms._asyncio(...)`)
+exclude_also = [
+ "from \\.api\\.",
+ "return .*\\._sync\\(",
+ "return await .*\\._asyncio\\(",
+]
+
+[tool.uv.build-backend]
+module-name = "liveblocks"
+module-root = ""
+exclude = ["README.mdx"]
+
+[build-system]
+requires = ["uv_build>=0.10.0,<0.11.0"]
+build-backend = "uv_build"
+
+[tool.ruff]
+line-length = 120
+target-version = "py311"
+
+[tool.ruff.lint]
+select = ["F", "I", "UP"]
\ No newline at end of file
diff --git a/packages/liveblocks-python/tests/conftest.py b/packages/liveblocks-python/tests/conftest.py
new file mode 100644
index 0000000000..10fb32345e
--- /dev/null
+++ b/packages/liveblocks-python/tests/conftest.py
@@ -0,0 +1,25 @@
+import pytest
+import respx
+
+from liveblocks.client import AsyncLiveblocks, Liveblocks
+
+
+@pytest.fixture(params=[Liveblocks, AsyncLiveblocks], ids=["sync", "async"])
+def client_cls(request):
+ return request.param
+
+
+@pytest.fixture
+def sync_client():
+ return Liveblocks(secret="sk_test_fake_key")
+
+
+@pytest.fixture
+def async_client():
+ return AsyncLiveblocks(secret="sk_test_fake_key")
+
+
+@pytest.fixture
+def mock_api():
+ with respx.mock(base_url="https://api.liveblocks.io") as respx_mock:
+ yield respx_mock
diff --git a/packages/liveblocks-python/tests/test_api.py b/packages/liveblocks-python/tests/test_api.py
new file mode 100644
index 0000000000..19a24fecf3
--- /dev/null
+++ b/packages/liveblocks-python/tests/test_api.py
@@ -0,0 +1,208 @@
+from __future__ import annotations
+
+import httpx
+import pytest
+
+from liveblocks.errors import LiveblocksError
+from liveblocks.models.create_room_request_body import CreateRoomRequestBody
+from liveblocks.models.room_permission_item import RoomPermissionItem
+
+ROOM_JSON = {
+ "id": "my-room",
+ "type": "room",
+ "createdAt": "2024-01-15T10:30:00Z",
+ "defaultAccesses": ["room:write"],
+ "usersAccesses": {},
+ "groupsAccesses": {},
+ "metadata": {},
+ "organizationId": "org_123456789",
+}
+
+
+# ---------------------------------------------------------------------------
+# GET /rooms
+# ---------------------------------------------------------------------------
+
+
+class TestGetRooms:
+ def test_sync_returns_parsed_response(self, sync_client, mock_api):
+ route = mock_api.get("/v2/rooms").mock(
+ return_value=httpx.Response(200, json={"nextCursor": None, "data": [ROOM_JSON]})
+ )
+ result = sync_client.get_rooms()
+
+ assert route.called
+ assert len(result.data) == 1
+ assert result.data[0].id == "my-room"
+ assert result.next_cursor is None
+
+ @pytest.mark.anyio
+ async def test_async_returns_parsed_response(self, async_client, mock_api):
+ mock_api.get("/v2/rooms").mock(
+ return_value=httpx.Response(200, json={"nextCursor": "cursor_abc", "data": [ROOM_JSON]})
+ )
+ result = await async_client.get_rooms()
+
+ assert len(result.data) == 1
+ assert result.data[0].id == "my-room"
+ assert result.next_cursor == "cursor_abc"
+
+ def test_passes_query_params(self, sync_client, mock_api):
+ route = mock_api.get("/v2/rooms").mock(return_value=httpx.Response(200, json={"nextCursor": None, "data": []}))
+ sync_client.get_rooms(limit=5, user_id="user-1")
+
+ request = route.calls.last.request
+ assert request.url.params["limit"] == "5"
+ assert request.url.params["userId"] == "user-1"
+
+ def test_empty_room_list(self, sync_client, mock_api):
+ mock_api.get("/v2/rooms").mock(return_value=httpx.Response(200, json={"nextCursor": None, "data": []}))
+ result = sync_client.get_rooms()
+
+ assert result.data == []
+
+
+# ---------------------------------------------------------------------------
+# GET /rooms/{room_id}
+# ---------------------------------------------------------------------------
+
+
+class TestGetRoom:
+ def test_sync_returns_room(self, sync_client, mock_api):
+ mock_api.get("/v2/rooms/my-room").mock(return_value=httpx.Response(200, json=ROOM_JSON))
+ room = sync_client.get_room("my-room")
+
+ assert room.id == "my-room"
+ assert room.type_.value == "room"
+
+ @pytest.mark.anyio
+ async def test_async_returns_room(self, async_client, mock_api):
+ mock_api.get("/v2/rooms/my-room").mock(return_value=httpx.Response(200, json=ROOM_JSON))
+ room = await async_client.get_room("my-room")
+
+ assert room.id == "my-room"
+
+ def test_url_encodes_room_id(self, sync_client, mock_api):
+ mock_api.get("/v2/rooms/room%2Fwith%2Fslashes").mock(
+ return_value=httpx.Response(200, json={**ROOM_JSON, "id": "room/with/slashes"})
+ )
+ room = sync_client.get_room("room/with/slashes")
+
+ assert room.id == "room/with/slashes"
+
+
+# ---------------------------------------------------------------------------
+# POST /rooms
+# ---------------------------------------------------------------------------
+
+
+class TestCreateRoom:
+ def test_sync_sends_body_and_returns_room(self, sync_client, mock_api):
+ route = mock_api.post("/v2/rooms").mock(return_value=httpx.Response(200, json=ROOM_JSON))
+ body = CreateRoomRequestBody.from_dict(
+ {
+ "id": "my-room",
+ "defaultAccesses": ["room:write"],
+ }
+ )
+ room = sync_client.create_room(body=body)
+
+ assert room.id == "my-room"
+ request = route.calls.last.request
+ assert request.headers["content-type"] == "application/json"
+
+ @pytest.mark.anyio
+ async def test_async_sends_body_and_returns_room(self, async_client, mock_api):
+ mock_api.post("/v2/rooms").mock(return_value=httpx.Response(200, json=ROOM_JSON))
+ body = CreateRoomRequestBody.from_dict(
+ {
+ "id": "my-room",
+ "defaultAccesses": ["room:write"],
+ }
+ )
+ room = await async_client.create_room(body=body)
+
+ assert room.id == "my-room"
+
+ def test_request_body_serialization(self, sync_client, mock_api):
+ route = mock_api.post("/v2/rooms").mock(return_value=httpx.Response(200, json=ROOM_JSON))
+ body = CreateRoomRequestBody(
+ id="new-room",
+ default_accesses=[RoomPermissionItem.ROOMWRITE],
+ )
+ sync_client.create_room(body=body)
+
+ import json
+
+ sent = json.loads(route.calls.last.request.content)
+ assert sent["id"] == "new-room"
+ assert sent["defaultAccesses"] == ["room:write"]
+
+
+# ---------------------------------------------------------------------------
+# DELETE /rooms/{room_id}
+# ---------------------------------------------------------------------------
+
+
+class TestDeleteRoom:
+ def test_sync_returns_none(self, sync_client, mock_api):
+ mock_api.delete("/v2/rooms/my-room").mock(return_value=httpx.Response(204))
+ result = sync_client.delete_room("my-room")
+
+ assert result is None
+
+ @pytest.mark.anyio
+ async def test_async_returns_none(self, async_client, mock_api):
+ mock_api.delete("/v2/rooms/my-room").mock(return_value=httpx.Response(204))
+ result = await async_client.delete_room("my-room")
+
+ assert result is None
+
+
+# ---------------------------------------------------------------------------
+# Error handling
+# ---------------------------------------------------------------------------
+
+
+class TestErrorHandling:
+ def test_403_raises_liveblocks_error(self, sync_client, mock_api):
+ mock_api.get("/v2/rooms").mock(return_value=httpx.Response(403, json={"message": "Forbidden"}))
+ with pytest.raises(LiveblocksError) as exc_info:
+ sync_client.get_rooms()
+
+ assert exc_info.value.status == 403
+ assert "Forbidden" in str(exc_info.value)
+
+ def test_404_raises_liveblocks_error(self, sync_client, mock_api):
+ mock_api.get("/v2/rooms/nonexistent").mock(return_value=httpx.Response(404, json={"message": "Room not found"}))
+ with pytest.raises(LiveblocksError) as exc_info:
+ sync_client.get_room("nonexistent")
+
+ assert exc_info.value.status == 404
+
+ def test_error_with_suggestion_and_docs(self, sync_client, mock_api):
+ mock_api.get("/v2/rooms").mock(
+ return_value=httpx.Response(
+ 401,
+ json={
+ "message": "Unauthorized",
+ "suggestion": "Check your API key",
+ "docs": "https://liveblocks.io/docs",
+ },
+ )
+ )
+ with pytest.raises(LiveblocksError) as exc_info:
+ sync_client.get_rooms()
+
+ assert exc_info.value.status == 401
+ assert exc_info.value.details is not None
+ assert "Check your API key" in exc_info.value.details
+ assert "https://liveblocks.io/docs" in exc_info.value.details
+
+ @pytest.mark.anyio
+ async def test_async_error_handling(self, async_client, mock_api):
+ mock_api.get("/v2/rooms").mock(return_value=httpx.Response(500, json={"message": "Internal Server Error"}))
+ with pytest.raises(LiveblocksError) as exc_info:
+ await async_client.get_rooms()
+
+ assert exc_info.value.status == 500
diff --git a/packages/liveblocks-python/tests/test_client.py b/packages/liveblocks-python/tests/test_client.py
new file mode 100644
index 0000000000..f502a3f62c
--- /dev/null
+++ b/packages/liveblocks-python/tests/test_client.py
@@ -0,0 +1,61 @@
+import pytest
+
+from liveblocks.client import AsyncLiveblocks, Liveblocks
+
+
+class TestConstructorValidation:
+ def test_rejects_invalid_prefix(self, client_cls):
+ with pytest.raises(ValueError, match="must start with 'sk_'"):
+ client_cls(secret="pk_abc123")
+
+ def test_rejects_invalid_chars(self, client_cls):
+ with pytest.raises(ValueError, match="Invalid chars"):
+ client_cls(secret="sk_abc 123!")
+
+ def test_accepts_valid_secret(self, client_cls):
+ client = client_cls(secret="sk_valid-key_123")
+ assert client._client.base_url == "https://api.liveblocks.io"
+
+ def test_accepts_custom_base_url(self, client_cls):
+ client = client_cls(secret="sk_test", base_url="https://custom.example.com")
+ assert str(client._client.base_url) == "https://custom.example.com"
+
+ def test_sets_auth_header(self, client_cls):
+ client = client_cls(secret="sk_my_secret")
+ assert client._client.headers["Authorization"] == "Bearer sk_my_secret"
+
+
+class TestSyncLifecycle:
+ def test_close(self):
+ client = Liveblocks(secret="sk_test")
+ client.close()
+ assert client._client.is_closed
+
+ def test_context_manager(self):
+ with Liveblocks(secret="sk_test") as client:
+ assert not client._client.is_closed
+ assert client._client.is_closed
+
+ def test_prepare_session(self):
+ client = Liveblocks(secret="sk_test")
+ session = client.prepare_session("user-1")
+ assert session._user_id == "user-1"
+
+
+class TestAsyncLifecycle:
+ @pytest.mark.anyio
+ async def test_close(self):
+ client = AsyncLiveblocks(secret="sk_test")
+ await client.close()
+ assert client._client.is_closed
+
+ @pytest.mark.anyio
+ async def test_context_manager(self):
+ async with AsyncLiveblocks(secret="sk_test") as client:
+ assert not client._client.is_closed
+ assert client._client.is_closed
+
+ def test_prepare_session(self):
+ client = AsyncLiveblocks(secret="sk_test")
+ session = client.prepare_session("user-1")
+ assert session._user_id == "user-1"
diff --git a/packages/liveblocks-python/tests/test_errors.py b/packages/liveblocks-python/tests/test_errors.py
new file mode 100644
index 0000000000..c4d6e97bc2
--- /dev/null
+++ b/packages/liveblocks-python/tests/test_errors.py
@@ -0,0 +1,60 @@
+from __future__ import annotations
+
+from unittest.mock import PropertyMock, patch
+
+import httpx
+
+from liveblocks.errors import LiveblocksError
+
+FALLBACK = "An error happened without an error message"
+
+
+class TestFromResponse:
+ def test_response_read_exception_uses_fallback(self):
+ response = httpx.Response(500, content=b"body")
+ with patch.object(type(response), "text", new_callable=PropertyMock, side_effect=Exception("read boom")):
+ err = LiveblocksError.from_response(response)
+
+ assert str(err) == f"{FALLBACK} (status 500)"
+ assert err.status == 500
+
+ def test_non_dict_json_uses_raw_text_as_message(self):
+ response = httpx.Response(400, text='"just a string"')
+ err = LiveblocksError.from_response(response)
+
+ assert str(err) == '"just a string" (status 400)'
+ assert err.status == 400
+ assert err.details is None
+
+ def test_unparseable_json_uses_raw_text_as_message(self):
+ response = httpx.Response(502, text="not json at all")
+ err = LiveblocksError.from_response(response)
+
+ assert str(err) == "not json at all (status 502)"
+ assert err.status == 502
+ assert err.details is None
+
+ def test_missing_message_key_uses_fallback(self):
+ response = httpx.Response(503, json={"error": "oops"})
+ err = LiveblocksError.from_response(response)
+
+ assert str(err) == f"{FALLBACK} (status 503)"
+ assert err.status == 503
+
+ def test_only_suggestion_no_docs(self):
+ response = httpx.Response(400, json={"message": "Bad", "suggestion": "Try again"})
+ err = LiveblocksError.from_response(response)
+
+ assert err.details == "Suggestion: Try again"
+
+ def test_only_docs_no_suggestion(self):
+ response = httpx.Response(400, json={"message": "Bad", "docs": "https://example.com"})
+ err = LiveblocksError.from_response(response)
+
+ assert err.details == "See also: https://example.com"
+
+ def test_no_suggestion_and_no_docs_gives_none_details(self):
+ response = httpx.Response(400, json={"message": "Bad"})
+ err = LiveblocksError.from_response(response)
+
+ assert err.details is None
diff --git a/packages/liveblocks-python/tests/test_models.py b/packages/liveblocks-python/tests/test_models.py
new file mode 100644
index 0000000000..a3e7dabd01
--- /dev/null
+++ b/packages/liveblocks-python/tests/test_models.py
@@ -0,0 +1,116 @@
+from __future__ import annotations
+
+from liveblocks.models.create_room_request_body import CreateRoomRequestBody
+from liveblocks.models.get_rooms_response import GetRoomsResponse
+from liveblocks.models.room import Room
+
+ROOM_DICT = {
+ "id": "my-room",
+ "type": "room",
+ "createdAt": "2024-01-15T10:30:00+00:00",
+ "defaultAccesses": ["room:write"],
+ "usersAccesses": {},
+ "groupsAccesses": {},
+ "metadata": {},
+ "organizationId": "org_123456789",
+}
+
+ROOM_DICT_FULL = {
+ "id": "full-room",
+ "type": "room",
+ "createdAt": "2024-06-01T12:00:00+00:00",
+ "lastConnectionAt": "2024-06-02T08:15:00+00:00",
+ "defaultAccesses": ["room:read", "room:presence:write"],
+ "usersAccesses": {"user-1": ["room:write"]},
+ "groupsAccesses": {"team-a": ["room:write"]},
+ "metadata": {"color": "blue", "tags": ["draft", "v2"]},
+ "organizationId": "org_123456789",
+}
+
+
+class TestRoomModel:
+ def test_from_dict_basic(self):
+ room = Room.from_dict(ROOM_DICT)
+
+ assert room.id == "my-room"
+ assert room.type_.value == "room"
+ assert len(room.default_accesses) == 1
+ assert room.default_accesses[0].value == "room:write"
+
+ def test_round_trip_basic(self):
+ room = Room.from_dict(ROOM_DICT)
+ result = room.to_dict()
+
+ assert result == ROOM_DICT
+
+ def test_round_trip_full(self):
+ room = Room.from_dict(ROOM_DICT_FULL)
+ result = room.to_dict()
+
+ assert result == ROOM_DICT_FULL
+
+ def test_metadata_access(self):
+ room = Room.from_dict(ROOM_DICT_FULL)
+
+ assert room.metadata["color"] == "blue"
+ assert room.metadata["tags"] == ["draft", "v2"]
+
+ def test_users_accesses(self):
+ room = Room.from_dict(ROOM_DICT_FULL)
+
+ assert "user-1" in room.users_accesses
+
+
+class TestGetRoomsResponseModel:
+ def test_from_dict_with_rooms(self):
+ data = {"nextCursor": "cursor_abc", "data": [ROOM_DICT]}
+ response = GetRoomsResponse.from_dict(data)
+
+ assert response.next_cursor == "cursor_abc"
+ assert len(response.data) == 1
+ assert response.data[0].id == "my-room"
+
+ def test_from_dict_empty(self):
+ data = {"nextCursor": None, "data": []}
+ response = GetRoomsResponse.from_dict(data)
+
+ assert response.next_cursor is None
+ assert response.data == []
+
+ def test_round_trip(self):
+ data = {"nextCursor": None, "data": [ROOM_DICT]}
+ response = GetRoomsResponse.from_dict(data)
+ result = response.to_dict()
+
+ assert result == data
+
+
+class TestCreateRoomRequestBodyModel:
+ def test_from_dict_minimal(self):
+ body = CreateRoomRequestBody.from_dict(
+ {
+ "id": "new-room",
+ "defaultAccesses": ["room:write"],
+ }
+ )
+
+ assert body.id == "new-room"
+ assert len(body.default_accesses) == 1
+
+ def test_round_trip_minimal(self):
+ original = {"id": "new-room", "defaultAccesses": ["room:write"]}
+ body = CreateRoomRequestBody.from_dict(original)
+ result = body.to_dict()
+
+ assert result == original
+
+ def test_round_trip_with_metadata(self):
+ original = {
+ "id": "new-room",
+ "defaultAccesses": ["room:write"],
+ "metadata": {"env": "staging"},
+ }
+ body = CreateRoomRequestBody.from_dict(original)
+ result = body.to_dict()
+
+ assert result == original
diff --git a/packages/liveblocks-python/tests/test_session.py b/packages/liveblocks-python/tests/test_session.py
new file mode 100644
index 0000000000..38cbe9275a
--- /dev/null
+++ b/packages/liveblocks-python/tests/test_session.py
@@ -0,0 +1,209 @@
+import warnings
+from unittest.mock import AsyncMock, MagicMock
+
+import pytest
+
+from liveblocks.client import AsyncLiveblocks, Liveblocks
+
+P1 = "room:read"
+P2 = "room:write"
+P3 = "comments:read"
+
+
+def make_session(
+ secret: str = "sk_testingtesting",
+ user_info: dict | None = None,
+ organization_id: str | None = None,
+):
+ client = Liveblocks(secret=secret)
+ return client.prepare_session("user-123", user_info=user_info, organization_id=organization_id)
+
+
+class TestConstructorValidation:
+ def test_rejects_empty_user_id(self):
+ client = Liveblocks(secret="sk_testingtesting")
+ with pytest.raises(ValueError, match="Invalid value for 'user_id'"):
+ client.prepare_session("")
+
+
+class TestPermissions:
+ def test_default_session_has_no_permissions(self):
+ assert make_session()._has_permissions is False
+
+ def test_adding_permissions_makes_has_permissions_true(self):
+ session = make_session()
+ assert session.allow("xyz", session.FULL_ACCESS)._has_permissions is True
+
+ def test_full_access_permissions(self):
+ session = make_session()
+ assert session.allow("xyz", session.FULL_ACCESS)._serialize_permissions() == {
+ "xyz": ["room:write", "comments:write"],
+ }
+
+ def test_read_access_permissions(self):
+ session = make_session()
+ assert session.allow("xyz", session.READ_ACCESS)._serialize_permissions() == {
+ "xyz": ["room:read", "room:presence:write", "comments:read"],
+ }
+
+ def test_raises_on_empty_room_name(self):
+ session = make_session()
+ with pytest.raises(ValueError, match="Invalid room name or pattern"):
+ session.allow("", session.READ_ACCESS)
+
+ def test_raises_on_room_name_too_long(self):
+ with pytest.raises(ValueError, match="Invalid room name or pattern"):
+ make_session().allow("a" * 129, [P1])
+
+ def test_raises_on_empty_permission_list(self):
+ with pytest.raises(ValueError, match="Permission list cannot be empty"):
+ make_session().allow("foobar", [])
+
+ def test_raises_on_asterisk_in_middle_of_room_name(self):
+ with pytest.raises(ValueError, match="Invalid room name or pattern"):
+ make_session().allow("foo*bar", [P1])
+
+ def test_allows_prefix_patterns(self):
+ assert make_session().allow("foobar*", [P1])._serialize_permissions() == {
+ "foobar*": [P1],
+ }
+
+ def test_allows_asterisk_as_pattern(self):
+ assert make_session().allow("*", [P1])._serialize_permissions() == {
+ "*": [P1],
+ }
+
+ def test_raises_on_invalid_permissions(self):
+ with pytest.raises(ValueError, match="Not a valid permission: x"):
+ make_session().allow("foobar*", ["x", "y"])
+
+ def test_permissions_are_additive(self):
+ assert (make_session().allow("foo", [P1]).allow("bar", [P2]).allow("foo", [P3])._serialize_permissions()) == {
+ "foo": [P1, P3],
+ "bar": [P2],
+ }
+
+ def test_permissions_are_deduped(self):
+ assert (
+ make_session()
+ .allow("r", [P1])
+ .allow("r", [P2, P3])
+ .allow("r", [P1, P3])
+ .allow("r", [P3])
+ .allow("r", [P3])
+ .allow("r", [P3])
+ .allow("r", [P3])
+ ._serialize_permissions()
+ ) == {
+ "r": [P1, P2, P3],
+ }
+
+ def test_raises_on_more_than_10_room_entries(self):
+ session = make_session()
+ for i in range(10):
+ session.allow(f"room{i}", [P1])
+
+ assert len(session._serialize_permissions()) == 10
+
+ with pytest.raises(RuntimeError, match="more than 10 rooms"):
+ session.allow("one-more-room", [P1])
+
+ # Adding to an existing entry is fine
+ session.allow("room7", [P2])
+
+
+class TestSealing:
+ def test_build_request_body_prevents_reuse(self):
+ session = make_session().allow("r", [P1]).allow("r", [P2, P3])
+
+ session._build_request_body()
+
+ with pytest.raises(RuntimeError, match="You cannot reuse Session instances"):
+ session._build_request_body()
+
+ def test_build_request_body_prevents_further_allow(self):
+ session = make_session().allow("r", [P1])
+
+ session._build_request_body()
+
+ with pytest.raises(RuntimeError, match="You can no longer change these permissions"):
+ session.allow("r", [P1])
+
+
+class TestSessionOptions:
+ def test_organization_id_can_be_set(self):
+ session = make_session(organization_id="org-123")
+ assert session.allow("room-1", [P1])._has_permissions is True
+
+ def test_user_info_can_be_set(self):
+ session = make_session(user_info={"name": "Ada"})
+ assert session.allow("room-1", [P1])._has_permissions is True
+
+ def test_organization_id_is_optional(self):
+ session = make_session()
+ assert session.allow("room-1", [P1])._has_permissions is True
+
+
+class TestBuildRequestBody:
+ def test_empty_permissions_emits_warning(self):
+ session = make_session()
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ session._build_request_body()
+
+ assert len(w) == 1
+ assert "Access tokens without any permission" in str(w[0].message)
+
+ def test_user_info_is_included_in_body(self):
+ session = make_session(user_info={"name": "Ada", "color": "blue"})
+ session.allow("room-1", [P1])
+ body = session._build_request_body()
+
+ assert body.user_info is not None
+ assert body.user_info.additional_properties == {"name": "Ada", "color": "blue"}
+
+ def test_organization_id_is_included_in_body(self):
+ session = make_session(organization_id="org-123")
+ session.allow("room-1", [P1])
+ body = session._build_request_body()
+
+ assert body.organization_id == "org-123"
+
+ def test_user_info_none_omits_field(self):
+ session = make_session(user_info=None)
+ session.allow("room-1", [P1])
+ body = session._build_request_body()
+
+ assert not isinstance(body.user_info, dict)
+
+ def test_organization_id_none_omits_field(self):
+ session = make_session(organization_id=None)
+ session.allow("room-1", [P1])
+ body = session._build_request_body()
+
+ assert not isinstance(body.organization_id, str)
+
+
+class TestAuthorize:
+ def test_sync_authorize_delegates_to_client(self):
+ client = Liveblocks(secret="sk_testingtesting")
+ client.authorize_user = MagicMock(return_value="mock_response")
+
+ session = client.prepare_session("user-123")
+ session.allow("room-1", [P1])
+ result = session.authorize()
+
+ assert result == "mock_response"
+ client.authorize_user.assert_called_once()
+
+ @pytest.mark.anyio
+ async def test_async_authorize_delegates_to_client(self):
+ client = AsyncLiveblocks(secret="sk_testingtesting")
+ client.authorize_user = AsyncMock(return_value="mock_response")
+
+ session = client.prepare_session("user-123")
+ session.allow("room-1", [P1])
+ result = await session.authorize()
+
+ assert result == "mock_response"
+ client.authorize_user.assert_called_once()
diff --git a/packages/liveblocks-python/tests/test_types.py b/packages/liveblocks-python/tests/test_types.py
new file mode 100644
index 0000000000..3a69f988c3
--- /dev/null
+++ b/packages/liveblocks-python/tests/test_types.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+import io
+
+from liveblocks.types import UNSET, File, Unset
+
+
+class TestUnset:
+ def test_bool_returns_false(self):
+ assert bool(Unset()) is False
+
+ def test_unset_singleton_is_falsy(self):
+ assert not UNSET
+
+
+class TestFile:
+ def test_to_tuple(self):
+ payload = io.BytesIO(b"hello")
+ f = File(payload=payload, file_name="test.txt", mime_type="text/plain")
+
+ result = f.to_tuple()
+
+ assert result == ("test.txt", payload, "text/plain")
+
+ def test_to_tuple_with_defaults(self):
+ payload = io.BytesIO(b"data")
+ f = File(payload=payload)
+
+ result = f.to_tuple()
+
+ assert result == (None, payload, None)
diff --git a/packages/liveblocks-python/tests/test_webhooks.py b/packages/liveblocks-python/tests/test_webhooks.py
new file mode 100644
index 0000000000..d239cf1225
--- /dev/null
+++ b/packages/liveblocks-python/tests/test_webhooks.py
@@ -0,0 +1,707 @@
+from __future__ import annotations
+
+import base64
+import hashlib
+import hmac
+import json
+from unittest.mock import patch
+
+import pytest
+
+from liveblocks.webhooks import (
+ WebhookHandler,
+ is_custom_notification_event,
+ is_text_mention_notification_event,
+ is_thread_notification_event,
+)
+
+SECRET = "whsec_sFOoBaR78ZZNyOl0TxbObFZWeo3rLg+d"
+
+USER_ENTERED_HEADERS = {
+ "webhook-id": "msg_2KvLUhLIHZtzZnNgUWv3PhGYf5f",
+ "webhook-timestamp": "1674850126",
+}
+
+USER_ENTERED_BODY = {
+ "data": {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "connectionId": 2196,
+ "enteredAt": "2023-01-27T20:08:40.693Z",
+ "numActiveUsers": 2,
+ "roomId": "hero-grid-12-01-2022",
+ "userId": "iepRYL2EWVHx8IcKVqhvZ6xljn",
+ "userInfo": None,
+ },
+ "type": "userEntered",
+}
+
+RAW_USER_ENTERED_BODY = json.dumps(USER_ENTERED_BODY, separators=(",", ":"))
+
+
+def _generate_signature(secret: str, message_id: str, timestamp: str, body: str) -> str:
+ """Reproduce the svix signing algorithm used by the TS tests."""
+ secret_key = base64.b64decode(secret[len("whsec_") :])
+ to_sign = f"{message_id}.{timestamp}.{body}".encode()
+ sig = base64.b64encode(hmac.new(secret_key, to_sign, hashlib.sha256).digest()).decode()
+ return f"v1,{sig}"
+
+
+# ---------------------------------------------------------------------------
+# Constructor validation
+# ---------------------------------------------------------------------------
+
+
+class TestConstructorValidation:
+ @pytest.mark.parametrize("invalid_secret", [None, "", "not_a_valid_secret"])
+ def test_rejects_invalid_secrets(self, invalid_secret):
+ with pytest.raises(ValueError):
+ WebhookHandler(invalid_secret)
+
+ def test_rejects_invalid_base64_after_prefix(self):
+ with pytest.raises(ValueError, match="invalid base64"):
+ WebhookHandler("whsec_!!!not-valid-base64!!!")
+
+
+# ---------------------------------------------------------------------------
+# verify_request
+# ---------------------------------------------------------------------------
+
+
+class TestVerifyRequest:
+ @pytest.mark.parametrize(
+ "event_type, data",
+ [
+ ("userEntered", USER_ENTERED_BODY["data"]),
+ (
+ "storageUpdated",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "hero-grid-12-01-2022",
+ "updatedAt": "2023-01-27T20:27:48.744Z",
+ },
+ ),
+ (
+ "userLeft",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "connectionId": 34597,
+ "leftAt": "2023-01-27T20:33:23.737Z",
+ "numActiveUsers": 4,
+ "roomId": "examples-hero-21-07-2022",
+ "userId": "zY8DF2NMqvKrzkuL5KkDIYY-da",
+ "userInfo": None,
+ },
+ ),
+ (
+ "roomCreated",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ "roomId": "examples-hero-21-07-2022",
+ },
+ ),
+ (
+ "roomDeleted",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "deletedAt": "2023-01-27T20:33:23.737Z",
+ "roomId": "examples-hero-21-07-2022",
+ },
+ ),
+ (
+ "commentCreated",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "commentId": "605a50b01a36d5ea7a2e9104",
+ "content": "Hello world",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ "createdBy": "authorId",
+ "roomId": "examples-hero-21-07-2022",
+ },
+ ),
+ (
+ "commentDeleted",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "commentId": "605a50b01a36d5ea7a2e9104",
+ "deletedAt": "2023-01-27T20:33:23.737Z",
+ "roomId": "examples-hero-21-07-2022",
+ },
+ ),
+ (
+ "commentEdited",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "commentId": "605a50b01a36d5ea7a2e9104",
+ "content": "Hello world",
+ "editedAt": "2023-01-27T20:33:23.737Z",
+ },
+ ),
+ (
+ "commentReactionAdded",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "commentId": "605a50b01a36d5ea7a2e9104",
+ "emoji": "\U0001f44d",
+ "roomId": "examples-hero-21-07-2022",
+ },
+ ),
+ (
+ "commentReactionRemoved",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "commentId": "605a50b01a36d5ea7a2e9104",
+ "emoji": "\U0001f44d",
+ "roomId": "examples-hero-21-07-2022",
+ },
+ ),
+ (
+ "threadMetadataUpdated",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "updatedBy": "authorId",
+ "updatedAt": "2023-01-27T20:33:23.737Z",
+ },
+ ),
+ (
+ "commentMetadataUpdated",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "commentId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "updatedBy": "authorId",
+ "updatedAt": "2023-01-27T20:33:23.737Z",
+ },
+ ),
+ (
+ "threadCreated",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "createdBy": "authorId",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ },
+ ),
+ (
+ "threadDeleted",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "deletedAt": "2023-01-27T20:33:23.737Z",
+ },
+ ),
+ (
+ "notification",
+ {
+ "kind": "thread",
+ "channel": "email",
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "inboxNotificationId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "userId": "userId",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ },
+ ),
+ (
+ "notification",
+ {
+ "kind": "textMention",
+ "channel": "email",
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "inboxNotificationId": "605a50b01a36d5ea7a2e9104",
+ "mentionId": "605a50b01a36d5ea7a2e9104",
+ "userId": "userId",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ },
+ ),
+ (
+ "notification",
+ {
+ "kind": "$custom",
+ "channel": "email",
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "inboxNotificationId": "605a50b01a36d5ea7a2e9104",
+ "subjectId": "605a50ba1a36d5ea7a2e9104",
+ "userId": "userId",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ },
+ ),
+ (
+ "threadMarkedAsResolved",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "updatedBy": "authorId",
+ "updatedAt": "2023-01-27T20:33:23.737Z",
+ },
+ ),
+ (
+ "threadMarkedAsUnresolved",
+ {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "updatedBy": "authorId",
+ "updatedAt": "2023-01-27T20:33:23.737Z",
+ },
+ ),
+ ],
+ ids=[
+ "userEntered",
+ "storageUpdated",
+ "userLeft",
+ "roomCreated",
+ "roomDeleted",
+ "commentCreated",
+ "commentDeleted",
+ "commentEdited",
+ "commentReactionAdded",
+ "commentReactionRemoved",
+ "threadMetadataUpdated",
+ "commentMetadataUpdated",
+ "threadCreated",
+ "threadDeleted",
+ "notification/thread",
+ "notification/textMention",
+ "notification/$custom",
+ "threadMarkedAsResolved",
+ "threadMarkedAsUnresolved",
+ ],
+ )
+ def test_verifies_event(self, event_type, data):
+ now_ms = 1674851609000
+ timestamp = str(now_ms // 1000)
+
+ body = {"data": data, "type": event_type}
+ raw_body = json.dumps(body, separators=(",", ":"))
+
+ webhook_id = "msg_2KvOK6yK9FO0U0nIyJYkM3jPwBs"
+ headers = {
+ "webhook-id": webhook_id,
+ "webhook-timestamp": timestamp,
+ "webhook-signature": _generate_signature(SECRET, webhook_id, timestamp, raw_body),
+ }
+
+ handler = WebhookHandler(SECRET)
+ with patch("liveblocks.webhooks.time.time", return_value=now_ms / 1000):
+ event = handler.verify_request(headers=headers, raw_body=raw_body)
+
+ assert event == body
+
+ def test_verifies_ydoc_updated_event(self):
+ ydoc_updated = {
+ "data": {
+ "appId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "hero-grid-12-01-2022",
+ "updatedAt": "2023-01-27T20:27:48.744Z",
+ },
+ "type": "ydocUpdated",
+ }
+ raw_body = json.dumps(ydoc_updated, separators=(",", ":"))
+
+ webhook_id = "msg_2KvOK6yK9FO0U0nIyJYkM3jPwBs"
+ timestamp = "1674851522"
+ headers = {
+ "webhook-id": webhook_id,
+ "webhook-timestamp": timestamp,
+ "webhook-signature": _generate_signature(SECRET, webhook_id, timestamp, raw_body),
+ }
+
+ handler = WebhookHandler(SECRET)
+ with patch("liveblocks.webhooks.time.time", return_value=1674851522):
+ event = handler.verify_request(headers=headers, raw_body=raw_body)
+
+ assert event == ydoc_updated
+
+ def test_verifies_event_with_multiple_signatures(self):
+ another_secret = "whsec_2KvOJ6yK9FO0hElL0JYkM3jPwBs="
+
+ sig1 = _generate_signature(
+ SECRET,
+ USER_ENTERED_HEADERS["webhook-id"],
+ USER_ENTERED_HEADERS["webhook-timestamp"],
+ RAW_USER_ENTERED_BODY,
+ )
+ sig2 = _generate_signature(
+ another_secret,
+ USER_ENTERED_HEADERS["webhook-id"],
+ USER_ENTERED_HEADERS["webhook-timestamp"],
+ RAW_USER_ENTERED_BODY,
+ )
+
+ headers = {
+ **USER_ENTERED_HEADERS,
+ "webhook-signature": f"{sig1} {sig2}",
+ }
+
+ handler = WebhookHandler(SECRET)
+ with patch("liveblocks.webhooks.time.time", return_value=1674850126):
+ event = handler.verify_request(headers=headers, raw_body=RAW_USER_ENTERED_BODY)
+
+ assert event == USER_ENTERED_BODY
+
+ def test_raises_on_non_string_raw_body(self):
+ sig = _generate_signature(
+ SECRET,
+ USER_ENTERED_HEADERS["webhook-id"],
+ USER_ENTERED_HEADERS["webhook-timestamp"],
+ RAW_USER_ENTERED_BODY,
+ )
+ headers = {**USER_ENTERED_HEADERS, "webhook-signature": sig}
+
+ handler = WebhookHandler(SECRET)
+ with patch("liveblocks.webhooks.time.time", return_value=1674850126):
+ with pytest.raises(ValueError, match="Invalid raw_body"):
+ handler.verify_request(headers=headers, raw_body={}) # type: ignore[arg-type]
+
+ def test_raises_on_invalid_signature(self):
+ headers = {
+ **USER_ENTERED_HEADERS,
+ "webhook-signature": "v1,invalid_signature",
+ }
+
+ handler = WebhookHandler(SECRET)
+ with patch("liveblocks.webhooks.time.time", return_value=1674850126):
+ with pytest.raises(ValueError, match="Invalid signature"):
+ handler.verify_request(headers=headers, raw_body=RAW_USER_ENTERED_BODY)
+
+ def test_raises_on_invalid_timestamp(self):
+ sig = _generate_signature(
+ SECRET,
+ USER_ENTERED_HEADERS["webhook-id"],
+ USER_ENTERED_HEADERS["webhook-timestamp"],
+ RAW_USER_ENTERED_BODY,
+ )
+ headers = {
+ **USER_ENTERED_HEADERS,
+ "webhook-signature": sig,
+ "webhook-timestamp": "invalid_timestamp",
+ }
+
+ handler = WebhookHandler(SECRET)
+ with patch("liveblocks.webhooks.time.time", return_value=1674850126):
+ with pytest.raises(ValueError, match="Invalid timestamp"):
+ handler.verify_request(headers=headers, raw_body=RAW_USER_ENTERED_BODY)
+
+ def test_raises_on_future_timestamp(self):
+ ten_minutes_ago_s = 1674850126 - 10 * 60
+
+ sig = _generate_signature(
+ SECRET,
+ USER_ENTERED_HEADERS["webhook-id"],
+ USER_ENTERED_HEADERS["webhook-timestamp"],
+ RAW_USER_ENTERED_BODY,
+ )
+ headers = {**USER_ENTERED_HEADERS, "webhook-signature": sig}
+
+ handler = WebhookHandler(SECRET)
+ with patch("liveblocks.webhooks.time.time", return_value=ten_minutes_ago_s):
+ with pytest.raises(ValueError, match="Timestamp in the future"):
+ handler.verify_request(headers=headers, raw_body=RAW_USER_ENTERED_BODY)
+
+ def test_raises_on_old_timestamp(self):
+ ten_minutes_later_s = 1674850126 + 10 * 60
+
+ sig = _generate_signature(
+ SECRET,
+ USER_ENTERED_HEADERS["webhook-id"],
+ USER_ENTERED_HEADERS["webhook-timestamp"],
+ RAW_USER_ENTERED_BODY,
+ )
+ headers = {**USER_ENTERED_HEADERS, "webhook-signature": sig}
+
+ handler = WebhookHandler(SECRET)
+ with patch("liveblocks.webhooks.time.time", return_value=ten_minutes_later_s):
+ with pytest.raises(ValueError, match="Timestamp too old"):
+ handler.verify_request(headers=headers, raw_body=RAW_USER_ENTERED_BODY)
+
+ def test_raises_on_unsupported_event_type(self):
+ body = {
+ "data": {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "hero-grid-12-01-2022",
+ "updatedAt": "2023-01-27T20:27:48.744Z",
+ },
+ "type": "unsupportedEventType",
+ }
+ raw_body = json.dumps(body, separators=(",", ":"))
+
+ webhook_id = "msg_2KvOK6yK9FO0U0nIyJYkM3jPwBs"
+ timestamp = "1674851522"
+ headers = {
+ "webhook-id": webhook_id,
+ "webhook-timestamp": timestamp,
+ "webhook-signature": _generate_signature(SECRET, webhook_id, timestamp, raw_body),
+ }
+
+ handler = WebhookHandler(SECRET)
+ with patch("liveblocks.webhooks.time.time", return_value=1674851522):
+ with pytest.raises(ValueError, match="Unknown event type"):
+ handler.verify_request(headers=headers, raw_body=raw_body)
+
+ def test_raises_on_unknown_notification_kind(self):
+ body = {
+ "data": {
+ "kind": "unknownKind",
+ "channel": "email",
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "userId": "userId",
+ "inboxNotificationId": "605a50b01a36d5ea7a2e9104",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ },
+ "type": "notification",
+ }
+ raw_body = json.dumps(body, separators=(",", ":"))
+
+ webhook_id = "msg_2KvOK6yK9FO0U0nIyJYkM3jPwBs"
+ timestamp = "1674851522"
+ headers = {
+ "webhook-id": webhook_id,
+ "webhook-timestamp": timestamp,
+ "webhook-signature": _generate_signature(SECRET, webhook_id, timestamp, raw_body),
+ }
+
+ handler = WebhookHandler(SECRET)
+ with patch("liveblocks.webhooks.time.time", return_value=1674851522):
+ with pytest.raises(ValueError, match="Unknown notification kind"):
+ handler.verify_request(headers=headers, raw_body=raw_body)
+
+ def test_raises_on_missing_webhook_id_header(self):
+ headers = {
+ "webhook-timestamp": "1674851522",
+ "webhook-signature": "v1,test",
+ }
+
+ handler = WebhookHandler(SECRET)
+ with pytest.raises(ValueError, match="Invalid webhook-id header"):
+ handler.verify_request(headers=headers, raw_body=RAW_USER_ENTERED_BODY)
+
+ def test_raises_on_missing_webhook_timestamp_header(self):
+ headers = {
+ "webhook-id": "msg_test",
+ "webhook-signature": "v1,test",
+ }
+
+ handler = WebhookHandler(SECRET)
+ with pytest.raises(ValueError, match="Invalid webhook-timestamp header"):
+ handler.verify_request(headers=headers, raw_body=RAW_USER_ENTERED_BODY)
+
+ def test_raises_on_missing_webhook_signature_header(self):
+ headers = {
+ "webhook-id": "msg_test",
+ "webhook-timestamp": "1674851522",
+ }
+
+ handler = WebhookHandler(SECRET)
+ with pytest.raises(ValueError, match="Invalid webhook-signature header"):
+ handler.verify_request(headers=headers, raw_body=RAW_USER_ENTERED_BODY)
+
+
+# ---------------------------------------------------------------------------
+# Type guards
+# ---------------------------------------------------------------------------
+
+
+class TestIsThreadNotificationEvent:
+ @pytest.mark.parametrize(
+ "name, event, expected",
+ [
+ (
+ "notification/thread",
+ {
+ "type": "notification",
+ "data": {
+ "kind": "thread",
+ "channel": "email",
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "inboxNotificationId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "userId": "userId",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ "triggeredAt": "2023-01-27T20:28:23.737Z",
+ },
+ },
+ True,
+ ),
+ (
+ "notification/textMention",
+ {
+ "type": "notification",
+ "data": {
+ "kind": "textMention",
+ "channel": "email",
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "inboxNotificationId": "605a50b01a36d5ea7a2e9104",
+ "mentionId": "605a50b01a36d5ea7a2e9104",
+ "userId": "userId",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ "triggeredAt": "2023-01-27T20:28:23.737Z",
+ },
+ },
+ False,
+ ),
+ (
+ "commentCreated",
+ {
+ "type": "commentCreated",
+ "data": {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "commentId": "605a50b01a36d5ea7a2e9104",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ "createdBy": "authorId",
+ "roomId": "examples-hero-21-07-2022",
+ },
+ },
+ False,
+ ),
+ ],
+ ids=["notification/thread", "notification/textMention", "commentCreated"],
+ )
+ def test_is_thread_notification(self, name, event, expected):
+ assert is_thread_notification_event(event) is expected
+
+
+class TestIsTextMentionNotificationEvent:
+ @pytest.mark.parametrize(
+ "name, event, expected",
+ [
+ (
+ "notification/textMention",
+ {
+ "type": "notification",
+ "data": {
+ "kind": "textMention",
+ "channel": "email",
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "inboxNotificationId": "605a50b01a36d5ea7a2e9104",
+ "mentionId": "605a50b01a36d5ea7a2e9104",
+ "userId": "userId",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ "triggeredAt": "2023-01-27T20:28:23.737Z",
+ },
+ },
+ True,
+ ),
+ (
+ "notification/thread",
+ {
+ "type": "notification",
+ "data": {
+ "kind": "thread",
+ "channel": "email",
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "inboxNotificationId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "userId": "userId",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ "triggeredAt": "2023-01-27T20:28:23.737Z",
+ },
+ },
+ False,
+ ),
+ (
+ "commentCreated",
+ {
+ "type": "commentCreated",
+ "data": {
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "commentId": "605a50b01a36d5ea7a2e9104",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ "createdBy": "authorId",
+ "roomId": "examples-hero-21-07-2022",
+ },
+ },
+ False,
+ ),
+ ],
+ ids=["notification/textMention", "notification/thread", "commentCreated"],
+ )
+ def test_is_text_mention_notification(self, name, event, expected):
+ assert is_text_mention_notification_event(event) is expected
+
+
+class TestIsCustomNotificationEvent:
+ @pytest.mark.parametrize(
+ "name, event, expected",
+ [
+ (
+ "notification/textMention",
+ {
+ "type": "notification",
+ "data": {
+ "kind": "textMention",
+ "channel": "email",
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "inboxNotificationId": "605a50b01a36d5ea7a2e9104",
+ "mentionId": "605a50b01a36d5ea7a2e9104",
+ "userId": "userId",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ "triggeredAt": "2023-01-27T20:28:23.737Z",
+ },
+ },
+ False,
+ ),
+ (
+ "notification/thread",
+ {
+ "type": "notification",
+ "data": {
+ "kind": "thread",
+ "channel": "email",
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "inboxNotificationId": "605a50b01a36d5ea7a2e9104",
+ "threadId": "605a50b01a36d5ea7a2e9104",
+ "userId": "userId",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ "triggeredAt": "2023-01-27T20:28:23.737Z",
+ },
+ },
+ False,
+ ),
+ (
+ "notification/$customKind",
+ {
+ "type": "notification",
+ "data": {
+ "kind": "$fileUploaded",
+ "channel": "email",
+ "projectId": "605a50b01a36d5ea7a2e9104",
+ "roomId": "examples-hero-21-07-2022",
+ "userId": "user-0",
+ "subjectId": "subject-0",
+ "inboxNotificationId": "605a50b01a36d5ea7a2e9104",
+ "createdAt": "2023-01-27T20:33:23.737Z",
+ "triggeredAt": "2023-01-27T20:28:23.737Z",
+ },
+ },
+ True,
+ ),
+ ],
+ ids=["notification/textMention", "notification/thread", "notification/$customKind"],
+ )
+ def test_is_custom_notification(self, name, event, expected):
+ assert is_custom_notification_event(event) is expected
diff --git a/packages/liveblocks-python/uv.lock b/packages/liveblocks-python/uv.lock
new file mode 100644
index 0000000000..632226721b
--- /dev/null
+++ b/packages/liveblocks-python/uv.lock
@@ -0,0 +1,386 @@
+version = 1
+revision = 3
+requires-python = ">=3.11"
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.2.25"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.13.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" },
+ { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" },
+ { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" },
+ { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" },
+ { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" },
+ { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" },
+ { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" },
+ { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" },
+ { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" },
+ { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" },
+ { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" },
+ { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" },
+ { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" },
+ { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" },
+ { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" },
+ { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" },
+ { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" },
+ { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" },
+ { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" },
+ { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" },
+ { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" },
+ { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" },
+ { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" },
+ { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" },
+ { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" },
+ { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
+ { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
+ { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
+ { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
+ { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
+ { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
+ { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
+ { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
+ { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
+ { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
+ { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
+ { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
+ { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "liveblocks"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "attrs" },
+ { name = "httpx" },
+ { name = "python-dateutil" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "respx" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "attrs", specifier = ">=22.2.0" },
+ { name = "httpx", specifier = ">=0.23.0,<0.29.0" },
+ { name = "python-dateutil", specifier = ">=2.8.0,<3" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "pytest", specifier = ">=9.0" },
+ { name = "pytest-cov", specifier = ">=7.0" },
+ { name = "respx", specifier = ">=0.22.0" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "respx"
+version = "0.22.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
diff --git a/packages/liveblocks-react-ui/src/components/Thread.tsx b/packages/liveblocks-react-ui/src/components/Thread.tsx
index 09c79c5234..ff4d27fa6c 100644
--- a/packages/liveblocks-react-ui/src/components/Thread.tsx
+++ b/packages/liveblocks-react-ui/src/components/Thread.tsx
@@ -175,6 +175,11 @@ export interface ThreadProps<
*/
showDeletedComments?: CommentProps["showDeleted"];
+ /**
+ * Whether to show the thread's subscription status.
+ */
+ showSubscription?: boolean;
+
/**
* Whether to show attachments.
*/
@@ -261,6 +266,7 @@ export const Thread = forwardRef(
showReactions = true,
showComposer = "collapsed",
showAttachments = true,
+ showSubscription = true,
showComposerFormattingControls = true,
maxVisibleComments,
commentDropdownItems,
@@ -604,7 +610,7 @@ export const Thread = forwardRef(
) : null
}
internalDropdownItems={
- isFirstComment ? (
+ isFirstComment && showSubscription ? (
-): string[] {
+function resolvePermissions_acc(
+ token: LiteAccessToken,
+ roomId: string
+): Permission[] {
// Try exact match first
- if (perms[roomId]) {
- return perms[roomId];
+ if (token.perms[roomId]) {
+ return token.perms[roomId];
}
// Try wildcard patterns
- for (const [pattern, scopes] of Object.entries(perms)) {
+ for (const [pattern, scopes] of Object.entries(token.perms)) {
if (pattern.endsWith("*")) {
const prefix = pattern.slice(0, -1);
if (roomId.startsWith(prefix)) {
@@ -44,10 +42,46 @@ function getScopesForRoom(
}
}
- // No matching permissions
return [];
}
+function resolvePermissions_id(
+ token: LiteIdToken,
+ roomId: string
+): Permission[] {
+ // ID token: resolve from the rooms DB (room must already exist)
+ const room = Rooms.getRoom(roomId);
+ if (!room) return [];
+
+ const scopes = new Set(room.defaultAccesses);
+
+ if (token.gids) {
+ for (const gid of token.gids) {
+ for (const p of room.groupsAccesses[gid] ?? []) {
+ scopes.add(p);
+ }
+ }
+ }
+
+ for (const p of room.usersAccesses[token.uid] ?? []) {
+ scopes.add(p);
+ }
+
+ return Array.from(scopes);
+}
+
+/**
+ * Resolves permissions for a token against a room.
+ * - Access tokens: match roomId against the token's explicit perms map.
+ * - ID tokens: look up the room in the DB and collect the union of
+ * defaultAccesses, groupsAccesses, and usersAccesses.
+ */
+function resolvePermissions(token: LiteToken, roomId: string): Permission[] {
+ return token.k === "acc"
+ ? resolvePermissions_acc(token, roomId)
+ : resolvePermissions_id(token, roomId);
+}
+
//
// HINT: Adjust this function to fit your application's needs.
// HINT: Return a valid object to allow the request, or `null` to disallow.
@@ -87,37 +121,21 @@ export function authorizeWebSocket(
return { ok: false }; // TODO Emit helpful X-Warn here, or not?
}
- if (payload.k === "acc") {
- const scopes = getScopesForRoom(roomId, payload.perms);
- if (scopes.length === 0) {
- return { ok: false }; // TODO Emit helpful X-Warn here, or not?
- }
-
- return {
- ok: true,
- roomId,
- ticketData: {
- version,
- id: payload.uid,
- info: payload.ui,
- scopes,
- },
- };
- } else if (payload.k === "id") {
- // TODO Warning that ID tokens are not fully supported yet
- return {
- ok: true,
- roomId,
- ticketData: {
- version,
- id: payload.uid,
- info: payload.ui,
- scopes: [Permission.Write],
- },
- };
+ const scopes = resolvePermissions(payload, roomId);
+ if (scopes.length === 0) {
+ return { ok: false };
}
- return { ok: false }; // TODO Emit helpful X-Warn here, or not?
+ return {
+ ok: true,
+ roomId,
+ ticketData: {
+ version,
+ id: payload.uid,
+ info: payload.ui,
+ scopes,
+ },
+ };
}
// Otherwise check if ?pubkey= is present (public key auth - anonymous user)
@@ -130,13 +148,19 @@ export function authorizeWebSocket(
};
}
+ // Auto-create the room if it doesn't exist yet
+ Rooms.getOrCreateRoom(roomId, {
+ defaultAccesses: [Permission.Write],
+ });
+
+ // Public key auth always grants write access (matches production behavior)
return {
ok: true,
roomId,
ticketData: {
version,
anonymousId: nanoid(),
- scopes: ["room:write"], // Public key auth always gets full write access
+ scopes: [Permission.Write],
},
};
}
diff --git a/tools/liveblocks-cli/src/dev-server/db/rooms.ts b/tools/liveblocks-cli/src/dev-server/db/rooms.ts
index 828b8a880e..5c96d2ed1f 100644
--- a/tools/liveblocks-cli/src/dev-server/db/rooms.ts
+++ b/tools/liveblocks-cli/src/dev-server/db/rooms.ts
@@ -15,141 +15,489 @@
* along with this program. If not, see .
*/
-import type { JsonObject, PlainLsonObject } from "@liveblocks/core";
+import type { JsonObject, Permission } from "@liveblocks/core";
+import { nanoid, WebsocketCloseCodes } from "@liveblocks/core";
import { DefaultMap, Room } from "@liveblocks/server";
-import { mkdirSync, mkdtempSync, readdirSync, rmSync } from "fs";
+import { Database } from "bun:sqlite";
+import { mkdirSync, mkdtempSync, rmSync } from "fs";
import { tmpdir } from "os";
-import { join, resolve } from "path";
+import { dirname, join, resolve } from "path";
import { BunSQLiteDriver } from "./BunSQLiteDriver";
-const DEFAULT_DB_PATH = ".liveblocks/v1/rooms";
-let basePath = DEFAULT_DB_PATH;
-let isEphemeral = false;
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
/**
- * Initialize the rooms database. When persist is false, a temp directory is
- * used and cleaned up on exit via `cleanup()`.
- *
- * Returns the root temp directory. Room data is stored in a `data/`
- * subdirectory so that sibling files (e.g. server.log) survive cleanup.
+ * Static room config (permissions, metadata) stored in the central rooms DB.
+ * Not to be confused with Room from @liveblocks/server, which is the live
+ * runtime instance managing WebSocket sessions and storage.
*/
-export function useEphemeralStorage(): string {
- const root = mkdtempSync(join(tmpdir(), "liveblocks-dev-"));
- basePath = join(root, "data");
- isEphemeral = true;
- return root;
-}
+export type DbRoom = {
+ id: string;
+ internalId: string;
+ organizationId: string;
+ defaultAccesses: Permission[];
+ usersAccesses: Record;
+ groupsAccesses: Record;
+ metadata: JsonObject;
+ createdAt: string;
+};
-export type RoomMeta = string; // Room metadata: just use the room ID
+export type RoomMeta = string;
export type SessionMeta = never;
-export type ClientMeta = JsonObject; // Public session metadata, sent to clients in ROOM_STATE
+export type ClientMeta = JsonObject;
+
+// ---------------------------------------------------------------------------
+// Module state
+// ---------------------------------------------------------------------------
+
+const DEFAULT_BASE_PATH = ".liveblocks/v1";
+let basePath = DEFAULT_BASE_PATH;
+let isEphemeral = false;
+let _initializedDb: Database | null = null;
+
+function roomsDir(): string {
+ return join(basePath, "rooms");
+}
+
+function ensureInit(): void {
+ if (_initializedDb) return;
+
+ const dbPath = join(basePath, "db.sql");
+ mkdirSync(dirname(dbPath), { recursive: true });
+
+ const db = new Database(dbPath, { create: true });
+ db.run("PRAGMA journal_mode = WAL");
+ db.run("PRAGMA foreign_keys = ON");
+
+ db.run(`CREATE TABLE IF NOT EXISTS rooms (
+ room_id TEXT NOT NULL PRIMARY KEY,
+ internal_id TEXT NOT NULL UNIQUE,
+ organization_id TEXT NOT NULL,
+ default_permissions TEXT NOT NULL,
+ metadata TEXT NOT NULL,
+ created_at TEXT NOT NULL
+ ) STRICT`);
+
+ db.run(`CREATE TABLE IF NOT EXISTS room_user_permissions (
+ room_id TEXT NOT NULL,
+ user_id TEXT NOT NULL,
+ scopes TEXT NOT NULL,
+ PRIMARY KEY (room_id, user_id),
+ FOREIGN KEY (room_id) REFERENCES rooms(room_id) ON DELETE CASCADE
+ ) STRICT`);
+
+ db.run(`CREATE TABLE IF NOT EXISTS room_group_permissions (
+ room_id TEXT NOT NULL,
+ group_id TEXT NOT NULL,
+ scopes TEXT NOT NULL,
+ PRIMARY KEY (room_id, group_id),
+ FOREIGN KEY (room_id) REFERENCES rooms(room_id) ON DELETE CASCADE
+ ) STRICT`);
+
+ _initializedDb = db;
+}
+
+function getDb(): Database {
+ if (!_initializedDb) throw new Error("Rooms DB not initialized");
+ return _initializedDb;
+}
+
+// ---------------------------------------------------------------------------
+// Internal SQL helpers
+// ---------------------------------------------------------------------------
+
+type RoomRow = {
+ room_id: string;
+ internal_id: string;
+ organization_id: string;
+ default_permissions: string;
+ metadata: string;
+ created_at: string;
+};
+
+type PermissionRow = {
+ room_id: string;
+ user_id?: string;
+ group_id?: string;
+ scopes: string;
+};
+
+function formatRoom(row: RoomRow): DbRoom {
+ const db = getDb();
+
+ const userPerms = db
+ .query<
+ PermissionRow,
+ [string]
+ >("SELECT user_id, scopes FROM room_user_permissions WHERE room_id = ?")
+ .all(row.room_id);
+
+ const groupPerms = db
+ .query<
+ PermissionRow,
+ [string]
+ >("SELECT group_id, scopes FROM room_group_permissions WHERE room_id = ?")
+ .all(row.room_id);
+
+ const usersAccesses: Record = {};
+ for (const r of userPerms) {
+ usersAccesses[r.user_id!] = JSON.parse(r.scopes) as Permission[];
+ }
+
+ const groupsAccesses: Record = {};
+ for (const r of groupPerms) {
+ groupsAccesses[r.group_id!] = JSON.parse(r.scopes) as Permission[];
+ }
+
+ return {
+ id: row.room_id,
+ internalId: row.internal_id,
+ organizationId: row.organization_id,
+ defaultAccesses: JSON.parse(row.default_permissions) as Permission[],
+ usersAccesses,
+ groupsAccesses,
+ metadata: JSON.parse(row.metadata) as JsonObject,
+ createdAt: row.created_at,
+ };
+}
+
+function getDbRoom(roomId: string): DbRoom | undefined {
+ const row = getDb()
+ .query<
+ RoomRow,
+ [string]
+ >("SELECT room_id, internal_id, organization_id, default_permissions, metadata, created_at FROM rooms WHERE room_id = ?")
+ .get(roomId);
+ if (!row) return undefined;
+ return formatRoom(row);
+}
+
+const DEFAULT_ORGANIZATION_ID = "default";
+
+function createDbRoom(
+ roomId: string,
+ opts?: {
+ organizationId?: string;
+ defaultAccesses?: Permission[];
+ metadata?: JsonObject;
+ usersAccesses?: Record;
+ groupsAccesses?: Record;
+ }
+): DbRoom {
+ const db = getDb();
+ const internalId = nanoid();
+ const now = new Date().toISOString();
+ const organizationId = opts?.organizationId ?? DEFAULT_ORGANIZATION_ID;
+ const defaultAccesses = opts?.defaultAccesses ?? ["room:write"];
+ const metadata = opts?.metadata ?? {};
+
+ db.run(
+ "INSERT INTO rooms (room_id, internal_id, organization_id, default_permissions, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?)",
+ [
+ roomId,
+ internalId,
+ organizationId,
+ JSON.stringify(defaultAccesses),
+ JSON.stringify(metadata),
+ now,
+ ]
+ );
+
+ if (opts?.usersAccesses) {
+ for (const [userId, scopes] of Object.entries(opts.usersAccesses)) {
+ db.run(
+ "INSERT INTO room_user_permissions (room_id, user_id, scopes) VALUES (?, ?, ?)",
+ [roomId, userId, JSON.stringify(scopes)]
+ );
+ }
+ }
+
+ if (opts?.groupsAccesses) {
+ for (const [groupId, scopes] of Object.entries(opts.groupsAccesses)) {
+ db.run(
+ "INSERT INTO room_group_permissions (room_id, group_id, scopes) VALUES (?, ?, ?)",
+ [roomId, groupId, JSON.stringify(scopes)]
+ );
+ }
+ }
+
+ return {
+ id: roomId,
+ internalId,
+ organizationId,
+ defaultAccesses: defaultAccesses as Permission[],
+ usersAccesses: opts?.usersAccesses ?? {},
+ groupsAccesses: opts?.groupsAccesses ?? {},
+ metadata,
+ createdAt: now,
+ };
+}
+
+function updateDbRoom(
+ roomId: string,
+ patch: {
+ defaultAccesses?: Permission[];
+ metadata?: JsonObject;
+ usersAccesses?: Record;
+ groupsAccesses?: Record;
+ }
+): DbRoom | undefined {
+ const db = getDb();
+ const existing = getDbRoom(roomId);
+ if (!existing) return undefined;
+
+ if (patch.defaultAccesses !== undefined) {
+ db.run("UPDATE rooms SET default_permissions = ? WHERE room_id = ?", [
+ JSON.stringify(patch.defaultAccesses),
+ roomId,
+ ]);
+ }
+
+ if (patch.metadata !== undefined) {
+ const merged = { ...existing.metadata, ...patch.metadata };
+ db.run("UPDATE rooms SET metadata = ? WHERE room_id = ?", [
+ JSON.stringify(merged),
+ roomId,
+ ]);
+ }
+
+ if (patch.usersAccesses !== undefined) {
+ for (const [userId, scopes] of Object.entries(patch.usersAccesses)) {
+ if (scopes === null) {
+ db.run(
+ "DELETE FROM room_user_permissions WHERE room_id = ? AND user_id = ?",
+ [roomId, userId]
+ );
+ } else {
+ db.run(
+ `INSERT INTO room_user_permissions (room_id, user_id, scopes) VALUES (?, ?, ?)
+ ON CONFLICT (room_id, user_id) DO UPDATE SET scopes = ?`,
+ [roomId, userId, JSON.stringify(scopes), JSON.stringify(scopes)]
+ );
+ }
+ }
+ }
+
+ if (patch.groupsAccesses !== undefined) {
+ for (const [groupId, scopes] of Object.entries(patch.groupsAccesses)) {
+ if (scopes === null) {
+ db.run(
+ "DELETE FROM room_group_permissions WHERE room_id = ? AND group_id = ?",
+ [roomId, groupId]
+ );
+ } else {
+ db.run(
+ `INSERT INTO room_group_permissions (room_id, group_id, scopes) VALUES (?, ?, ?)
+ ON CONFLICT (room_id, group_id) DO UPDATE SET scopes = ?`,
+ [roomId, groupId, JSON.stringify(scopes), JSON.stringify(scopes)]
+ );
+ }
+ }
+ }
+
+ return getDbRoom(roomId);
+}
+
+function deleteDbRoom(roomId: string): void {
+ getDb().run("DELETE FROM rooms WHERE room_id = ?", [roomId]);
+}
+
+// ---------------------------------------------------------------------------
+// Room instances (in-memory Room objects backed by per-room storage files)
+// ---------------------------------------------------------------------------
+
+function getStoragePath(internalId: string): string {
+ const dir = roomsDir();
+ const resolved = resolve(dir, `${internalId}.sql`);
+ if (!resolved.startsWith(resolve(dir) + "/")) {
+ throw new Error("Invalid internal ID");
+ }
+ return resolved;
+}
-// Stores a list of all "loaded room instances"
const instances = new DefaultMap<
string,
Room
>((roomId) => {
- mkdirSync(basePath, { recursive: true });
- const storage = new BunSQLiteDriver(getSqlitePath(roomId));
+ const record = getDbRoom(roomId) ?? createDbRoom(roomId);
+
+ mkdirSync(roomsDir(), { recursive: true });
+ const storage = new BunSQLiteDriver(getStoragePath(record.internalId));
const room = new Room(roomId, {
storage,
- // hooks: {
- // onSessionDidStart(session) {
- // const numSessions = room.numSessions;
- // console.log(`Users in room: ${numSessions - 1} → ${numSessions}`);
- // },
- //
- // onSessionDidEnd(session) {
- // const numSessions = room.numSessions;
- // console.log(`Users in room: ${numSessions + 1} → ${numSessions}`);
- // },
- // },
});
return room;
});
-function getSqlitePath(roomId: string): string {
- const resolved = resolve(basePath, `${encodeURIComponent(roomId)}.db`);
- if (!resolved.startsWith(resolve(basePath) + "/")) {
- throw new Error("Invalid room ID");
- }
- return resolved;
-}
+// ---------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------
/**
- * Get or create a room instance by ID.
+ * Switch to ephemeral (temp dir) storage. Returns the root temp directory.
+ * Room data is stored in a `data/` subdirectory so that sibling files
+ * (e.g. server.log) survive cleanup.
*/
-export function getOrCreate(
- roomId: string
-): Room {
- return instances.getOrCreate(roomId);
+export function useEphemeralStorage(): string {
+ const root = mkdtempSync(join(tmpdir(), "liveblocks-dev-"));
+ basePath = join(root, "data");
+ isEphemeral = true;
+ return root;
}
/**
- * Check if a room exists by checking if its database file exists.
+ * Get a room record from the DB. Returns undefined if not found.
*/
-export async function exists(roomId: string): Promise {
- const dbPath = getSqlitePath(roomId);
- const file = Bun.file(dbPath);
- return await file.exists();
+export function getRoom(roomId: string): DbRoom | undefined {
+ ensureInit();
+ return getDbRoom(roomId);
}
/**
- * Get all room IDs by scanning the directory for .db files.
+ * Get a room record, creating one (with DB row + storage file) if it
+ * doesn't exist yet. Accepts optional overrides only used on creation.
*/
-export function getAll(): string[] {
- try {
- mkdirSync(basePath, { recursive: true });
- const files = readdirSync(basePath);
- const roomIds = files
- .filter((file) => file.endsWith(".db"))
- .map((file) => decodeURIComponent(file.replace(/\.db$/, "")));
- return roomIds;
- } catch (error) {
- // Directory doesn't exist or can't be read
- return [];
+export function getOrCreateRoom(
+ roomId: string,
+ opts?: {
+ organizationId?: string;
+ defaultAccesses?: Permission[];
+ metadata?: JsonObject;
+ usersAccesses?: Record;
+ groupsAccesses?: Record;
}
+): DbRoom {
+ ensureInit();
+ const existing = getDbRoom(roomId);
+ if (existing) return existing;
+
+ const record = createDbRoom(roomId, opts);
+ // Ensure the storage file is created via the Room instance
+ instances.getOrCreate(roomId);
+ return record;
}
+export type RoomFilters = {
+ organizationId?: string;
+ roomId?: { value: string; operator: "^" };
+ metadata?: Record;
+};
+
/**
- * Create a room with empty storage.
+ * List all room records, optionally filtered by metadata and/or roomId prefix.
*/
-export async function create(roomId: string): Promise {
- if (await exists(roomId)) {
- throw new Error(`Room with id "${roomId}" already exists`);
+export function listRooms(filters?: RoomFilters): DbRoom[] {
+ ensureInit();
+
+ const db = getDb();
+ const conditions: string[] = [];
+ const params: string[] = [];
+
+ if (filters?.organizationId) {
+ conditions.push("organization_id = ?");
+ params.push(filters.organizationId);
}
- const room = instances.getOrCreate(roomId);
- await room.load();
+ if (filters?.roomId) {
+ // Only prefix operator is supported
+ conditions.push("room_id LIKE ? ESCAPE '\\'");
+ // Escape any existing % or _ in the prefix, then append %
+ const escaped = filters.roomId.value.replace(/[%_\\]/g, "\\$&");
+ params.push(`${escaped}%`);
+ }
- // Initialize empty storage
- const emptyStorage: PlainLsonObject = {
- liveblocksType: "LiveObject",
- data: {},
- };
- await room.driver.DANGEROUSLY_reset_nodes(emptyStorage);
- room.unload();
+ if (filters?.metadata) {
+ for (const [key, value] of Object.entries(filters.metadata)) {
+ conditions.push("JSON_EXTRACT(metadata, ?) = ?");
+ params.push(`$.${JSON.stringify(key)}`, value);
+ }
+ }
+
+ const where =
+ conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
+ const rows = db
+ .query(
+ `SELECT room_id, internal_id, organization_id, default_permissions, metadata, created_at
+ FROM rooms
+ ${where}`
+ )
+ .all(...params);
+
+ return rows.map(formatRoom);
}
/**
- * Delete a room by removing its database file.
+ * Update a room's metadata/permissions in the DB.
*/
-export async function remove(roomId: string): Promise {
- instances.get(roomId)?.unload();
- instances.delete(roomId);
- const path = getSqlitePath(roomId);
- try {
- await Bun.write(path, ""); // Clear the file
- await Bun.file(path).unlink(); // Delete the file
- } catch (error) {
- // File might not exist, ignore
+export function updateRoom(
+ roomId: string,
+ patch: {
+ defaultAccesses?: Permission[];
+ metadata?: JsonObject;
+ usersAccesses?: Record;
+ groupsAccesses?: Record;
}
+): DbRoom | undefined {
+ ensureInit();
+ return updateDbRoom(roomId, patch);
}
/**
- * Unload all room instances and clear the instances map, but keep SQLite
- * files on disk so storage survives a reboot.
+ * Delete a room: remove from DB (CASCADE) and delete storage file.
+ */
+export async function deleteRoom(roomId: string): Promise {
+ ensureInit();
+ const record = getDbRoom(roomId);
+
+ const room = instances.get(roomId);
+ if (room) {
+ // TODO: Ideally, unload() should require the room to be in maintenance
+ // mode first and kick users from the room as an implementation detail,
+ // which would block new incoming connections before kicking existing
+ // sessions. Our production backend already enforces this via the
+ // onRoomWillUnload hook, but the dev server doesn't have maintenance mode
+ // implemented yet. Until then, we kick all sessions here to prevent them
+ // from using stale/closed storage drivers.
+ room.endSessionBy(
+ () => true,
+ WebsocketCloseCodes.KICKED,
+ "Deliberately disconnected"
+ );
+ room.unload();
+ (room.driver as BunSQLiteDriver).close();
+ instances.delete(roomId);
+ }
+
+ if (record) {
+ const path = getStoragePath(record.internalId);
+ try {
+ await Bun.write(path, "");
+ await Bun.file(path).unlink();
+ } catch {
+ // File might not exist, ignore
+ }
+
+ deleteDbRoom(roomId);
+ }
+}
+
+/**
+ * Get the in-memory Room instance for managing real-time sessions (WebSocket
+ * connections, presence, storage operations). Creates the DB record + storage
+ * file if the room doesn't exist yet.
+ */
+export function getRoomInstance(
+ roomId: string
+): Room {
+ ensureInit();
+ return instances.getOrCreate(roomId);
+}
+
+/**
+ * Unload all room instances but keep files on disk.
*/
export function unloadAll(): void {
for (const room of instances.values()) {
@@ -159,15 +507,20 @@ export function unloadAll(): void {
}
/**
- * Unload all room instances and, if in ephemeral mode, remove the temp
- * directory.
+ * Unload all room instances and, if ephemeral, remove the temp directory.
*/
export function cleanup(): void {
for (const room of instances.values()) {
room.unload();
+ (room.driver as BunSQLiteDriver).close();
}
instances.clear();
+ if (_initializedDb) {
+ _initializedDb.close();
+ _initializedDb = null;
+ }
+
if (isEphemeral) {
rmSync(basePath, { recursive: true, force: true });
}
diff --git a/tools/liveblocks-cli/src/dev-server/index.ts b/tools/liveblocks-cli/src/dev-server/index.ts
index 52f60bec29..d41a84c0f8 100644
--- a/tools/liveblocks-cli/src/dev-server/index.ts
+++ b/tools/liveblocks-cli/src/dev-server/index.ts
@@ -27,7 +27,7 @@ import { bold, dim, green, red, yellow } from "~/lib/term-colors";
import { authorizeWebSocket } from "./auth";
import type { ClientMeta, RoomMeta, SessionMeta } from "./db/rooms";
-import * as RoomsDB from "./db/rooms";
+import * as Rooms from "./db/rooms";
import {
buildFixPrompt,
checkLiveblocksSetup,
@@ -165,7 +165,7 @@ const dev: SubCommand = {
const hostname =
options.host || process.env.LIVEBLOCKS_DEVSERVER_HOST || "localhost";
- const ephemeralPath = ephemeral ? RoomsDB.useEphemeralStorage() : null;
+ const ephemeralPath = ephemeral ? Rooms.useEphemeralStorage() : null;
if (await isPortInUse(port, hostname)) {
console.error(
@@ -199,7 +199,7 @@ const dev: SubCommand = {
const { roomId, ticketData } = authResult;
// Look up or create the room for the requested room ID
- const room = RoomsDB.getOrCreate(roomId);
+ const room = Rooms.getRoomInstance(roomId);
await room.load();
const ticket = await room.createTicket(ticketData);
@@ -301,7 +301,7 @@ const dev: SubCommand = {
server = createServer();
async function reboot() {
- RoomsDB.unloadAll();
+ Rooms.unloadAll();
await server.stop(true);
server = createServer();
console.log("Crash \uD83D\uDCA5");
@@ -356,7 +356,7 @@ const dev: SubCommand = {
} finally {
void logFile.end();
await server.stop();
- RoomsDB.cleanup();
+ Rooms.cleanup();
stderr(dim("Liveblocks dev server shut down"));
}
process.exit(code);
@@ -384,7 +384,7 @@ const dev: SubCommand = {
const ch = data.toString();
if (ch === "q" || ch === "\x03" /* Ctrl-C */) {
void server.stop(true).then(() => {
- RoomsDB.cleanup();
+ Rooms.cleanup();
process.exit(0);
});
} else if (ch === "!") {
diff --git a/tools/liveblocks-cli/src/dev-server/lib/jwt-lite.ts b/tools/liveblocks-cli/src/dev-server/lib/jwt-lite.ts
index f16e0421a8..79745f2144 100644
--- a/tools/liveblocks-cli/src/dev-server/lib/jwt-lite.ts
+++ b/tools/liveblocks-cli/src/dev-server/lib/jwt-lite.ts
@@ -15,17 +15,56 @@
* along with this program. If not, see .
*/
-import type { IUserInfo, JsonObject, Permission } from "@liveblocks/core";
-import { nanoid } from "@liveblocks/core";
-
-// Local dev only (lite version of) access token
-export type LiteAccessToken = {
- k: "acc";
- pid: "localdev";
- uid: string; // user id
- ui?: IUserInfo; // user info
- perms: Record; // permissions by room ID or room pattern
-};
+import type { DistributiveOmit } from "@liveblocks/core";
+import { nanoid, Permission, tryParseJson } from "@liveblocks/core";
+import type { DecoderType } from "decoders";
+import {
+ array,
+ constant,
+ enum_,
+ number,
+ object,
+ optional,
+ record,
+ string,
+ taggedUnion,
+} from "decoders";
+
+import { userInfo } from "./decoders";
+
+const unsignedJwtHeader = object({
+ alg: constant("none"),
+});
+
+const liteAccessToken = object({
+ k: constant("acc"),
+ pid: constant("localdev"),
+ uid: string,
+ ui: optional(userInfo),
+ perms: record(array(enum_(Permission))),
+ exp: number,
+});
+
+const liteIdToken = object({
+ k: constant("id"),
+ pid: constant("localdev"),
+ uid: string,
+ ui: optional(userInfo),
+ gids: optional(array(string)),
+ exp: number,
+});
+
+const liteToken = taggedUnion("k", {
+ acc: liteAccessToken,
+ id: liteIdToken,
+});
+
+export type LiteAccessToken = DecoderType;
+export type LiteIdToken = DecoderType;
+export type LiteToken = DecoderType;
+
+// Input type for createJwtLite (exp is added automatically)
+export type LiteTokenInput = DistributiveOmit;
// Simple base64url encoding (no padding)
function base64Encode(str: string): string {
@@ -45,7 +84,7 @@ function base64Decode(str: string): string {
* "alg: none" (no cryptographic signature). For local development only.
* Always sets pid to 'localdev'.
*/
-export function createJwtLite(info: LiteAccessToken): string {
+export function createJwtLite(info: LiteTokenInput): string {
const nowSecs = Math.floor(Date.now() / 1000);
const payload = {
...info,
@@ -65,39 +104,25 @@ export function createJwtLite(info: LiteAccessToken): string {
* Only accepts tokens with pid 'localdev'.
* Returns the payload if valid, null otherwise.
*/
-export function verifyJwtLite(token: string): LiteAccessToken | null {
+export function verifyJwtLite(token: string): LiteToken | null {
const parts = token.split(".");
if (parts.length !== 3) return null;
const [headerB64, payloadB64, signature] = parts;
// Verify it's an unsigned token (alg: none)
- try {
- const header = JSON.parse(base64Decode(headerB64)) as JsonObject;
- if (header.alg !== "none") return null;
- } catch {
- return null;
- }
-
- // Decode payload
- let payload: Record;
- try {
- payload = JSON.parse(base64Decode(payloadB64)) as JsonObject;
- } catch {
+ if (signature !== "") return null; // Signature must be empty for alg: none
+ if (!unsignedJwtHeader.value(tryParseJson(base64Decode(headerB64))))
return null;
- }
-
- // Signature must be empty for alg: none
- if (signature !== "") return null;
- // Only accept tokens for localdev project
- if (payload.pid !== "localdev") return null;
+ // Decode and validate payload using the decoder
+ const payload = liteToken.value(tryParseJson(base64Decode(payloadB64)));
+ if (!payload) return null;
// Check expiration
- const exp = payload.exp;
- if (typeof exp === "number" && exp < Math.floor(Date.now() / 1000)) {
+ if (payload.exp < Math.floor(Date.now() / 1000)) {
return null;
}
- return payload as LiteAccessToken;
+ return payload;
}
diff --git a/tools/liveblocks-cli/src/dev-server/routes/auth.ts b/tools/liveblocks-cli/src/dev-server/routes/auth.ts
index 23e16bf395..42827c16dc 100644
--- a/tools/liveblocks-cli/src/dev-server/routes/auth.ts
+++ b/tools/liveblocks-cli/src/dev-server/routes/auth.ts
@@ -22,8 +22,6 @@ import { array, enum_, object, optional, record, string } from "decoders";
import { authorizeSecretKey } from "~/dev-server/lib/auth";
import { userInfo } from "~/dev-server/lib/decoders";
import { createJwtLite } from "~/dev-server/lib/jwt-lite";
-import { NOT_IMPLEMENTED } from "~/dev-server/responses";
-
// Valid permission values (from @liveblocks/core Permission enum)
const permission = enum_(Permission);
@@ -52,8 +50,23 @@ zen.route(
}
);
-zen.route("POST /v2/identify-user", () =>
- NOT_IMPLEMENTED(
- "ID tokens are not supported in the Liveblocks dev server. To develop locally, use access tokens instead (via POST /v2/authorize-user)"
- )
+zen.route(
+ "POST /v2/identify-user",
+
+ object({
+ userId: string,
+ userInfo: optional(userInfo),
+ groupIds: optional(array(string)),
+ }),
+
+ ({ body }) => {
+ const token = createJwtLite({
+ k: "id",
+ pid: "localdev",
+ uid: body.userId,
+ ui: body.userInfo,
+ gids: body.groupIds,
+ });
+ return { token };
+ }
);
diff --git a/tools/liveblocks-cli/src/dev-server/routes/rest-api.ts b/tools/liveblocks-cli/src/dev-server/routes/rest-api.ts
index 3e355f4bc3..4fe99b00a0 100644
--- a/tools/liveblocks-cli/src/dev-server/routes/rest-api.ts
+++ b/tools/liveblocks-cli/src/dev-server/routes/rest-api.ts
@@ -15,7 +15,8 @@
* along with this program. If not, see .
*/
-import type { PlainLsonObject } from "@liveblocks/core";
+import type { JsonObject, Permission, PlainLsonObject } from "@liveblocks/core";
+import { QueryParser } from "@liveblocks/query-parser";
import type { Guid, Logger, YDocId } from "@liveblocks/server";
import {
jsonObjectYolo,
@@ -24,14 +25,25 @@ import {
snapshotToPlainLson_eager,
} from "@liveblocks/server";
import { json, ZenRouter } from "@liveblocks/zenrouter";
-import { constant, enum_, object, string } from "decoders";
+import {
+ array,
+ constant,
+ either,
+ enum_,
+ nullable,
+ object,
+ optional,
+ record,
+ string,
+} from "decoders";
import { Base64 } from "js-base64";
import * as Y from "yjs";
-import * as RoomsDB from "~/dev-server/db/rooms";
+import type { DbRoom, RoomFilters } from "~/dev-server/db/rooms";
+import * as Rooms from "~/dev-server/db/rooms";
import { authorizeSecretKey } from "~/dev-server/lib/auth";
import { yDocToJson } from "~/dev-server/lib/ydoc";
-import { NOT_IMPLEMENTED, XWARN } from "~/dev-server/responses";
+import { NOT_IMPLEMENTED } from "~/dev-server/responses";
enum SerializationFormat {
PlainLson = "plain-lson", // the default
@@ -40,6 +52,12 @@ enum SerializationFormat {
const serializationFormat = enum_(SerializationFormat);
+const roomMetadata = record(
+ string,
+ // Values can be either strings or arrays-of-strings
+ either(string, array(string))
+);
+
export const zen = new ZenRouter({
authorize: ({ req }) => authorizeSecretKey(req),
});
@@ -51,46 +69,109 @@ function ROOM_NOT_FOUND(roomId: string): Response {
);
}
-/**
- * Get a room by ID
- */
-zen.route("GET /v2/rooms/", async ({ p }) => {
- const exists = await RoomsDB.exists(p.roomId);
- if (!exists) {
- throw ROOM_NOT_FOUND(p.roomId);
+const roomsQueryParser = new QueryParser({
+ fields: { roomId: "string" },
+ indexableFields: { metadata: "token" },
+});
+
+function parseRoomsQuery(query: string): RoomFilters {
+ const result = roomsQueryParser.parse(query);
+ const filters: RoomFilters = {};
+
+ for (const cond of result.query.allOf) {
+ switch (cond.type) {
+ case "PrefixCondition": {
+ if (
+ cond.field.type === "DirectField" &&
+ cond.field.ref.name === "roomId"
+ ) {
+ filters.roomId = { value: cond.prefix.value, operator: "^" };
+ }
+ break;
+ }
+ case "ExactCondition": {
+ if (
+ cond.field.type === "KeyedField" &&
+ cond.field.base.name === "metadata" &&
+ cond.value.type === "LiteralString"
+ ) {
+ filters.metadata ??= {};
+ filters.metadata[cond.field.key] = cond.value.value;
+ }
+ break;
+ }
+ }
}
+ return filters;
+}
+
+function serializeRoom(record: DbRoom) {
return {
type: "room",
- id: p.roomId,
- createdAt: new Date().toISOString(),
- metadata: {},
- defaultAccesses: ["room:write"],
- groupsAccesses: {},
- usersAccesses: {},
+ id: record.id,
+ organizationId: record.organizationId,
+ createdAt: record.createdAt,
+ metadata: record.metadata,
+ defaultAccesses: record.defaultAccesses,
+ groupsAccesses: record.groupsAccesses,
+ usersAccesses: record.usersAccesses,
};
+}
+
+/**
+ * Get a room by ID
+ */
+zen.route("GET /v2/rooms/", ({ p }) => {
+ const record = Rooms.getRoom(p.roomId);
+ if (!record) {
+ throw ROOM_NOT_FOUND(p.roomId);
+ }
+ return serializeRoom(record);
});
/**
* Get all rooms
*/
-zen.route("GET /v2/rooms", () => {
- const roomIds = RoomsDB.getAll();
- const data = roomIds.map((id) => ({
- type: "room",
- id,
- createdAt: new Date().toISOString(),
- metadata: {},
- defaultAccesses: ["room:write"],
- groupsAccesses: {},
- usersAccesses: {},
- }));
+zen.route("GET /v2/rooms", ({ url }) => {
+ let filters: RoomFilters | undefined;
- return XWARN(
- { data, nextPage: null, nextCursor: null },
- 200,
- "The Liveblocks dev server doesn't implement room permissions or pagination yet."
- );
+ // The `query` param uses the Liveblocks query language
+ const query = url.searchParams.get("query");
+ if (query) {
+ try {
+ filters = parseRoomsQuery(query);
+ } catch (err) {
+ return json(
+ {
+ error: "INVALID_QUERY",
+ message: err instanceof Error ? err.message : String(err),
+ },
+ 422
+ );
+ }
+ } else {
+ // Fall back to simple metadata.* query params
+ const metadata: Record = {};
+ for (const [key, value] of url.searchParams.entries()) {
+ if (key.startsWith("metadata.")) {
+ metadata[key.slice("metadata.".length)] = value;
+ }
+ }
+ if (Object.keys(metadata).length > 0) {
+ filters = { metadata };
+ }
+ }
+
+ const organizationId = url.searchParams.get("organizationId") ?? undefined;
+ if (organizationId) {
+ filters = { ...filters, organizationId };
+ }
+
+ const records = Rooms.listRooms(filters);
+ const data = records.map(serializeRoom);
+
+ return { data, nextPage: null, nextCursor: null };
});
/**
@@ -103,55 +184,66 @@ zen.route(
"POST /v2/rooms",
object({
id: string,
+ organizationId: optional(string),
+ defaultAccesses: optional(array(string)),
+ metadata: optional(roomMetadata),
+ usersAccesses: optional(record(string, array(string))),
+ groupsAccesses: optional(record(string, array(string))),
}),
- async ({ body }) => {
- // Check if room already exists
- const exists = await RoomsDB.exists(body.id);
- if (exists) {
- return new Response(
- JSON.stringify({
+ ({ body }) => {
+ if (Rooms.getRoom(body.id)) {
+ return json(
+ {
error: "ROOM_ALREADY_EXISTS",
message: `Room with id "${body.id}" already exists.`,
- }),
- {
- status: 409,
- headers: { "Content-Type": "application/json" },
- }
+ },
+ 409
);
}
- // Create room with empty storage
- try {
- await RoomsDB.create(body.id);
- } catch (error) {
- if (error instanceof Error && error.message.includes("already exists")) {
- return new Response(
- JSON.stringify({
- error: "ROOM_ALREADY_EXISTS",
- message: `Room with id "${body.id}" already exists.`,
- }),
- {
- status: 409,
- headers: { "Content-Type": "application/json" },
- }
- );
- }
- throw error;
- }
-
- return {
- type: "room",
- id: body.id,
- createdAt: new Date().toISOString(),
- metadata: {},
- defaultAccesses: ["room:write"],
- groupsAccesses: {},
- usersAccesses: {},
- };
+ const rec = Rooms.getOrCreateRoom(body.id, {
+ organizationId: body.organizationId,
+ defaultAccesses: body.defaultAccesses as Permission[] | undefined,
+ metadata: body.metadata as JsonObject | undefined,
+ usersAccesses: body.usersAccesses as
+ | Record
+ | undefined,
+ groupsAccesses: body.groupsAccesses as
+ | Record
+ | undefined,
+ });
+ return serializeRoom(rec);
}
);
-zen.route("POST /v2/rooms/", () => NOT_IMPLEMENTED());
+/**
+ * Update a room
+ */
+zen.route(
+ "POST /v2/rooms/",
+ object({
+ defaultAccesses: optional(array(string)),
+ metadata: optional(roomMetadata),
+ usersAccesses: optional(record(string, nullable(array(string)))),
+ groupsAccesses: optional(record(string, nullable(array(string)))),
+ }),
+ ({ p, body }) => {
+ const updated = Rooms.updateRoom(p.roomId, {
+ defaultAccesses: body.defaultAccesses as Permission[] | undefined,
+ metadata: body.metadata as JsonObject | undefined,
+ usersAccesses: body.usersAccesses as
+ | Record
+ | undefined,
+ groupsAccesses: body.groupsAccesses as
+ | Record
+ | undefined,
+ });
+ if (!updated) {
+ throw ROOM_NOT_FOUND(p.roomId);
+ }
+ return serializeRoom(updated);
+ }
+);
/**
* Delete a room
@@ -159,7 +251,7 @@ zen.route("POST /v2/rooms/", () => NOT_IMPLEMENTED());
* Idempotent: returns 204 even if the room doesn't exist.
*/
zen.route("DELETE /v2/rooms/", async ({ p }) => {
- await RoomsDB.remove(p.roomId);
+ await Rooms.deleteRoom(p.roomId);
return new Response(null, { status: 204 });
});
@@ -167,8 +259,7 @@ zen.route("DELETE /v2/rooms/", async ({ p }) => {
* Get storage for a room
*/
zen.route("GET /v2/rooms//storage", async ({ url, p }) => {
- const exists = await RoomsDB.exists(p.roomId);
- if (!exists) {
+ if (!Rooms.getRoom(p.roomId)) {
throw ROOM_NOT_FOUND(p.roomId);
}
@@ -176,7 +267,7 @@ zen.route("GET /v2/rooms//storage", async ({ url, p }) => {
serializationFormat.value(url.searchParams.get("format")) ??
SerializationFormat.PlainLson;
- const room = RoomsDB.getOrCreate(p.roomId);
+ const room = Rooms.getRoomInstance(p.roomId);
await room.load();
const snapshot = room.storage.loadedDriver.get_snapshot(false);
@@ -206,12 +297,11 @@ zen.route(
}).refineType(),
async ({ p, body }) => {
- const exists = await RoomsDB.exists(p.roomId);
- if (!exists) {
+ if (!Rooms.getRoom(p.roomId)) {
throw ROOM_NOT_FOUND(p.roomId);
}
- const room = RoomsDB.getOrCreate(p.roomId);
+ const room = Rooms.getRoomInstance(p.roomId);
await room.load();
// Check if storage already has data
@@ -248,12 +338,11 @@ zen.route(
* Delete storage for a room (reset to empty)
*/
zen.route("DELETE /v2/rooms//storage", async ({ p }) => {
- const exists = await RoomsDB.exists(p.roomId);
- if (!exists) {
+ if (!Rooms.getRoom(p.roomId)) {
throw ROOM_NOT_FOUND(p.roomId);
}
- const room = RoomsDB.getOrCreate(p.roomId);
+ const room = Rooms.getRoomInstance(p.roomId);
await room.load();
const emptyStorage: PlainLsonObject = {
@@ -267,12 +356,11 @@ zen.route("DELETE /v2/rooms//storage", async ({ p }) => {
});
zen.route("GET /v2/rooms//ydoc", async ({ url, p }) => {
- const exists = await RoomsDB.exists(p.roomId);
- if (!exists) {
+ if (!Rooms.getRoom(p.roomId)) {
throw ROOM_NOT_FOUND(p.roomId);
}
- const room = RoomsDB.getOrCreate(p.roomId);
+ const room = Rooms.getRoomInstance(p.roomId);
await room.load();
const key = url.searchParams.get("key") ?? "";
@@ -304,12 +392,11 @@ zen.route("PUT /v2/rooms//ydoc", async ({ req, url, p }) => {
);
}
- const exists = await RoomsDB.exists(p.roomId);
- if (!exists) {
+ if (!Rooms.getRoom(p.roomId)) {
throw ROOM_NOT_FOUND(p.roomId);
}
- const room = RoomsDB.getOrCreate(p.roomId);
+ const room = Rooms.getRoomInstance(p.roomId);
await room.load();
const buffer: ArrayBuffer = await req.arrayBuffer();
@@ -348,12 +435,11 @@ zen.route("PUT /v2/rooms//ydoc", async ({ req, url, p }) => {
});
zen.route("GET /v2/rooms//ydoc-binary", async ({ url, p }) => {
- const exists = await RoomsDB.exists(p.roomId);
- if (!exists) {
+ if (!Rooms.getRoom(p.roomId)) {
throw ROOM_NOT_FOUND(p.roomId);
}
- const room = RoomsDB.getOrCreate(p.roomId);
+ const room = Rooms.getRoomInstance(p.roomId);
await room.load();
const ydocId = (url.searchParams.get("guid") ?? ROOT_YDOC_ID) as YDocId;
@@ -371,6 +457,25 @@ zen.route("GET /v2/rooms//ydoc-binary", async ({ url, p }) => {
});
});
+/**
+ * Get active users in a room
+ */
+zen.route("GET /v2/rooms//active_users", ({ p }) => {
+ if (!Rooms.getRoom(p.roomId)) {
+ throw ROOM_NOT_FOUND(p.roomId);
+ }
+
+ const room = Rooms.getRoomInstance(p.roomId);
+ const data = room.listSessions().map((session) => ({
+ type: "user",
+ connectionId: session.actor,
+ id: session.user.id,
+ info: session.user.info,
+ }));
+
+ return json({ data });
+});
+
/**
* ------------------------------------------------------------
* NOT IMPLEMENTED ROUTES
@@ -388,7 +493,6 @@ zen.route("GET /v2/rooms//ydoc-binary", async ({ url, p }) => {
zen.route("POST /v2/rooms//update-organization-id", () => NOT_IMPLEMENTED());
zen.route("GET /v2/rooms//prewarm", () => NOT_IMPLEMENTED());
zen.route("POST /v2/rooms//request-storage-mutation", () => NOT_IMPLEMENTED());
- zen.route("GET /v2/rooms//active_users", () => NOT_IMPLEMENTED());
zen.route("POST /v2/rooms//send-message", () => NOT_IMPLEMENTED());
zen.route("POST /v2/rooms//broadcast_event", () => NOT_IMPLEMENTED());
zen.route("GET /v2/rooms//versions", () => NOT_IMPLEMENTED());
diff --git a/tools/liveblocks-cli/test/devserver/auth.test.ts b/tools/liveblocks-cli/test/devserver/auth.test.ts
new file mode 100644
index 0000000000..d20b35460b
--- /dev/null
+++ b/tools/liveblocks-cli/test/devserver/auth.test.ts
@@ -0,0 +1,399 @@
+/**
+ * Copyright (c) Liveblocks Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Permission } from "@liveblocks/core";
+import { afterAll, beforeAll, describe, expect, test } from "bun:test";
+
+import { authorizeWebSocket } from "~/dev-server/auth";
+import * as Rooms from "~/dev-server/db/rooms";
+import { createJwtLite, verifyJwtLite } from "~/dev-server/lib/jwt-lite";
+import { zen } from "~/dev-server/routes/auth";
+
+const BASE = "http://localhost";
+const AUTH = { Authorization: "Bearer sk_localdev" };
+
+async function post(
+ path: string,
+ body: unknown
+): Promise<{ status: number; body: Record }> {
+ const resp = await zen.fetch(
+ new Request(`${BASE}${path}`, {
+ method: "POST",
+ headers: { ...AUTH, "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ })
+ );
+ return {
+ status: resp.status,
+ body: (await resp.json()) as Record,
+ };
+}
+
+describe("POST /v2/authorize-user", () => {
+ test("returns a valid access token", async () => {
+ const { status, body } = await post("/v2/authorize-user", {
+ userId: "user-1",
+ permissions: { "room-*": ["room:write"] },
+ });
+
+ expect(status).toBe(200);
+ expect(body.token).toBeString();
+
+ const payload = verifyJwtLite(body.token as string);
+ expect(payload).not.toBeNull();
+ expect(payload!.k).toBe("acc");
+ expect(payload!.uid).toBe("user-1");
+ if (payload!.k === "acc") {
+ expect(payload!.perms).toEqual({ "room-*": ["room:write"] });
+ }
+ });
+
+ test("includes userInfo when provided", async () => {
+ const { status, body } = await post("/v2/authorize-user", {
+ userId: "user-2",
+ userInfo: { name: "Alice", avatar: "https://example.com/alice.png" },
+ permissions: { "*": ["room:write"] },
+ });
+
+ expect(status).toBe(200);
+ const payload = verifyJwtLite(body.token as string);
+ expect(payload!.ui).toEqual({
+ name: "Alice",
+ avatar: "https://example.com/alice.png",
+ });
+ });
+
+ test("rejects missing userId", async () => {
+ const { status } = await post("/v2/authorize-user", {
+ permissions: { "*": ["room:write"] },
+ });
+ expect(status).toBe(422);
+ });
+
+ test("rejects missing permissions", async () => {
+ const { status } = await post("/v2/authorize-user", {
+ userId: "user-1",
+ });
+ expect(status).toBe(422);
+ });
+
+ test("rejects without auth header", async () => {
+ const resp = await zen.fetch(
+ new Request(`${BASE}/v2/authorize-user`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ userId: "user-1",
+ permissions: { "*": ["room:write"] },
+ }),
+ })
+ );
+ expect(resp.status).toBe(401);
+ });
+});
+
+describe("POST /v2/identify-user", () => {
+ test("returns a valid ID token", async () => {
+ const { status, body } = await post("/v2/identify-user", {
+ userId: "user-1",
+ });
+
+ expect(status).toBe(200);
+ expect(body.token).toBeString();
+
+ const payload = verifyJwtLite(body.token as string);
+ expect(payload).not.toBeNull();
+ expect(payload!.k).toBe("id");
+ expect(payload!.uid).toBe("user-1");
+ });
+
+ test("includes userInfo when provided", async () => {
+ const { status, body } = await post("/v2/identify-user", {
+ userId: "user-2",
+ userInfo: { name: "Bob" },
+ });
+
+ expect(status).toBe(200);
+ const payload = verifyJwtLite(body.token as string);
+ expect(payload!.ui).toEqual({ name: "Bob" });
+ });
+
+ test("rejects missing userId", async () => {
+ const { status } = await post("/v2/identify-user", {});
+ expect(status).toBe(422);
+ });
+
+ test("rejects without auth header", async () => {
+ const resp = await zen.fetch(
+ new Request(`${BASE}/v2/identify-user`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ userId: "user-1" }),
+ })
+ );
+ expect(resp.status).toBe(401);
+ });
+
+ test("includes groupIds when provided", async () => {
+ const { status, body } = await post("/v2/identify-user", {
+ userId: "user-3",
+ groupIds: ["team-a", "team-b"],
+ });
+
+ expect(status).toBe(200);
+ const payload = verifyJwtLite(body.token as string);
+ expect(payload).not.toBeNull();
+ expect(payload!.k).toBe("id");
+ if (payload!.k === "id") {
+ expect(payload!.gids).toEqual(["team-a", "team-b"]);
+ }
+ });
+});
+
+// ---------------------------------------------------------------------------
+// authorizeWebSocket — permission resolution
+// ---------------------------------------------------------------------------
+
+function wsReq(params: Record, version = "v8"): Request {
+ const url = new URL(`http://localhost/${version}`);
+ for (const [k, v] of Object.entries(params)) {
+ url.searchParams.set(k, v);
+ }
+ return new Request(url, { headers: { Upgrade: "websocket" } });
+}
+
+function accToken(userId: string, perms: Record): string {
+ return createJwtLite({
+ k: "acc",
+ pid: "localdev",
+ uid: userId,
+ perms,
+ });
+}
+
+function idToken(userId: string, groupIds?: string[]): string {
+ return createJwtLite({
+ k: "id",
+ pid: "localdev",
+ uid: userId,
+ gids: groupIds,
+ });
+}
+
+describe("authorizeWebSocket", () => {
+ beforeAll(() => {
+ Rooms.useEphemeralStorage();
+ });
+
+ afterAll(() => {
+ Rooms.cleanup();
+ });
+
+ // -- Edge cases --
+
+ test("rejects missing roomId", () => {
+ const result = authorizeWebSocket(wsReq({}));
+ expect(result.ok).toBe(false);
+ });
+
+ test("rejects invalid protocol version", () => {
+ const result = authorizeWebSocket(wsReq({ roomId: "room-1" }, "v99"));
+ expect(result.ok).toBe(false);
+ });
+
+ test("rejects invalid token", () => {
+ const result = authorizeWebSocket(
+ wsReq({ roomId: "room-1", tok: "garbage" })
+ );
+ expect(result.ok).toBe(false);
+ });
+
+ // -- Access token resolution --
+
+ describe("access tokens", () => {
+ test("exact room match grants scopes", () => {
+ const tok = accToken("user-1", { "my-room": [Permission.Write] });
+ const result = authorizeWebSocket(wsReq({ roomId: "my-room", tok }));
+ expect(result.ok).toBe(true);
+ if (result.ok) {
+ expect(result.ticketData.scopes).toEqual([Permission.Write]);
+ }
+ });
+
+ test("wildcard pattern match grants scopes", () => {
+ const tok = accToken("user-1", { "project-*": [Permission.Write] });
+ const result = authorizeWebSocket(wsReq({ roomId: "project-abc", tok }));
+ expect(result.ok).toBe(true);
+ if (result.ok) {
+ expect(result.ticketData.scopes).toEqual([Permission.Write]);
+ }
+ });
+
+ test("no matching room/pattern is denied", () => {
+ const tok = accToken("user-1", { "other-room": [Permission.Write] });
+ const result = authorizeWebSocket(wsReq({ roomId: "my-room", tok }));
+ expect(result.ok).toBe(false);
+ });
+
+ test("read-only permission in token gives read-only scopes", () => {
+ const tok = accToken("user-1", { "my-room": [Permission.Read] });
+ const result = authorizeWebSocket(wsReq({ roomId: "my-room", tok }));
+ expect(result.ok).toBe(true);
+ if (result.ok) {
+ expect(result.ticketData.scopes).toEqual([Permission.Read]);
+ }
+ });
+ });
+
+ // -- ID token resolution --
+
+ describe("ID tokens", () => {
+ test("write room grants write scopes", () => {
+ Rooms.getOrCreateRoom("id-write-room", {
+ defaultAccesses: [Permission.Write],
+ });
+ const tok = idToken("user-1");
+ const result = authorizeWebSocket(
+ wsReq({ roomId: "id-write-room", tok })
+ );
+ expect(result.ok).toBe(true);
+ if (result.ok) {
+ expect(result.ticketData.scopes).toEqual([Permission.Write]);
+ }
+ });
+
+ test("read-only room grants read-only scopes", () => {
+ Rooms.getOrCreateRoom("id-readonly-room", {
+ defaultAccesses: [Permission.Read],
+ });
+ const tok = idToken("user-1");
+ const result = authorizeWebSocket(
+ wsReq({ roomId: "id-readonly-room", tok })
+ );
+ expect(result.ok).toBe(true);
+ if (result.ok) {
+ expect(result.ticketData.scopes).toEqual([Permission.Read]);
+ }
+ });
+
+ test("nonexistent room is denied", () => {
+ const tok = idToken("user-1");
+ const result = authorizeWebSocket(wsReq({ roomId: "no-such-room", tok }));
+ expect(result.ok).toBe(false);
+ });
+
+ test("usersAccesses override for specific user", () => {
+ Rooms.getOrCreateRoom("id-user-override-room", {
+ defaultAccesses: [Permission.Read],
+ usersAccesses: { "vip-user": [Permission.Write] },
+ });
+ // VIP user gets both read (from default) and write (from usersAccesses)
+ const tok1 = idToken("vip-user");
+ const result1 = authorizeWebSocket(
+ wsReq({ roomId: "id-user-override-room", tok: tok1 })
+ );
+ expect(result1.ok).toBe(true);
+ if (result1.ok) {
+ expect(result1.ticketData.scopes).toContain(Permission.Read);
+ expect(result1.ticketData.scopes).toContain(Permission.Write);
+ }
+ // Regular user gets read only
+ const tok2 = idToken("regular-user");
+ const result2 = authorizeWebSocket(
+ wsReq({ roomId: "id-user-override-room", tok: tok2 })
+ );
+ expect(result2.ok).toBe(true);
+ if (result2.ok) {
+ expect(result2.ticketData.scopes).toEqual([Permission.Read]);
+ }
+ });
+
+ test("groupsAccesses grants group-based permissions", () => {
+ Rooms.getOrCreateRoom("id-group-room", {
+ defaultAccesses: [],
+ groupsAccesses: { "team-a": [Permission.Write] },
+ });
+ // Member of team-a gets write
+ const tok1 = idToken("user-1", ["team-a"]);
+ const result1 = authorizeWebSocket(
+ wsReq({ roomId: "id-group-room", tok: tok1 })
+ );
+ expect(result1.ok).toBe(true);
+ if (result1.ok) {
+ expect(result1.ticketData.scopes).toContain(Permission.Write);
+ }
+ // Non-member gets denied (empty defaultAccesses)
+ const tok2 = idToken("user-2");
+ const result2 = authorizeWebSocket(
+ wsReq({ roomId: "id-group-room", tok: tok2 })
+ );
+ expect(result2.ok).toBe(false);
+ });
+
+ test("union of defaultAccesses + groupsAccesses", () => {
+ Rooms.getOrCreateRoom("id-union-room", {
+ defaultAccesses: [Permission.Read],
+ groupsAccesses: { editors: [Permission.Write] },
+ });
+ const tok = idToken("user-1", ["editors"]);
+ const result = authorizeWebSocket(
+ wsReq({ roomId: "id-union-room", tok })
+ );
+ expect(result.ok).toBe(true);
+ if (result.ok) {
+ expect(result.ticketData.scopes).toContain(Permission.Read);
+ expect(result.ticketData.scopes).toContain(Permission.Write);
+ }
+ });
+ });
+
+ // -- Pubkey auth --
+
+ describe("pubkey auth", () => {
+ test("nonexistent room is auto-created with write access", () => {
+ const result = authorizeWebSocket(
+ wsReq({ roomId: "pk-new-room", pubkey: "pk_localdev" })
+ );
+ expect(result.ok).toBe(true);
+ if (result.ok) {
+ expect(result.ticketData.scopes).toEqual([Permission.Write]);
+ }
+ });
+
+ test("always grants write even on a read-only room", () => {
+ // Explicitly create a room with read-only default access
+ Rooms.getOrCreateRoom("pk-readonly-room", {
+ defaultAccesses: [Permission.Read],
+ });
+ // Pubkey auth ignores the room's defaultAccesses and always grants write
+ const result = authorizeWebSocket(
+ wsReq({ roomId: "pk-readonly-room", pubkey: "pk_localdev" })
+ );
+ expect(result.ok).toBe(true);
+ if (result.ok) {
+ expect(result.ticketData.scopes).toEqual([Permission.Write]);
+ }
+ });
+
+ test("wrong pubkey is denied", () => {
+ const result = authorizeWebSocket(
+ wsReq({ roomId: "pk-some-room", pubkey: "pk_wrong" })
+ );
+ expect(result.ok).toBe(false);
+ });
+ });
+});
diff --git a/tools/liveblocks-cli/test/dev-server-cmd.test.ts b/tools/liveblocks-cli/test/devserver/cmd.test.ts
similarity index 97%
rename from tools/liveblocks-cli/test/dev-server-cmd.test.ts
rename to tools/liveblocks-cli/test/devserver/cmd.test.ts
index cd361d65cd..dd4009c0b3 100644
--- a/tools/liveblocks-cli/test/dev-server-cmd.test.ts
+++ b/tools/liveblocks-cli/test/devserver/cmd.test.ts
@@ -20,8 +20,8 @@ import { resolve } from "node:path";
import { beforeAll, describe, expect, test } from "bun:test";
-const CLI = resolve(import.meta.dir, "../dist/index.js");
-const CWD = resolve(import.meta.dir, "..");
+const CLI = resolve(import.meta.dir, "../../dist/index.js");
+const CWD = resolve(import.meta.dir, "../..");
/**
* Runs the equivalent of `npx liveblocks ` (but on the local build
diff --git a/tools/liveblocks-cli/test/devserver/rest-api/rooms.test.ts b/tools/liveblocks-cli/test/devserver/rest-api/rooms.test.ts
new file mode 100644
index 0000000000..bb43a70ca1
--- /dev/null
+++ b/tools/liveblocks-cli/test/devserver/rest-api/rooms.test.ts
@@ -0,0 +1,310 @@
+/**
+ * Copyright (c) Liveblocks Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { afterAll, beforeAll, describe, expect, test } from "bun:test";
+
+import * as Rooms from "~/dev-server/db/rooms";
+import { zen } from "~/dev-server/routes/rest-api";
+
+const BASE = "http://localhost";
+const AUTH = { Authorization: "Bearer sk_localdev" };
+
+type ApiResult = { status: number; body: Record };
+
+async function api(
+ method: string,
+ path: string,
+ body?: unknown
+): Promise {
+ const headers: Record = { ...AUTH };
+ const init: RequestInit = { method, headers };
+ if (body !== undefined) {
+ headers["Content-Type"] = "application/json";
+ init.body = JSON.stringify(body);
+ }
+ const resp = await zen.fetch(new Request(`${BASE}${path}`, init));
+ const respBody: Record = resp.headers
+ .get("content-type")
+ ?.includes("application/json")
+ ? ((await resp.json()) as Record)
+ : {};
+ return { status: resp.status, body: respBody };
+}
+
+describe("REST API - rooms", () => {
+ beforeAll(() => {
+ Rooms.useEphemeralStorage();
+ });
+
+ afterAll(() => {
+ Rooms.cleanup();
+ });
+
+ test("create three rooms with different metadata, then list/filter", async () => {
+ // Create three rooms with distinct metadata
+ expect(
+ (
+ await api("POST", "/v2/rooms", {
+ id: "project-alpha-1",
+ metadata: {
+ team: "frontend",
+ priority: "high",
+ labels: ["bug", "urgent"],
+ },
+ })
+ ).status
+ ).toBe(200);
+
+ expect(
+ (
+ await api("POST", "/v2/rooms", {
+ id: "project-alpha-2",
+ metadata: { team: "backend", priority: "low" },
+ })
+ ).status
+ ).toBe(200);
+
+ expect(
+ (
+ await api("POST", "/v2/rooms", {
+ id: "project-beta-1",
+ metadata: { team: "frontend", priority: "low" },
+ })
+ ).status
+ ).toBe(200);
+
+ // List all rooms (no filters)
+ const all = await api("GET", "/v2/rooms");
+ expect(all.status).toBe(200);
+ expect(all.body.data).toHaveLength(3);
+
+ // Filter by roomId prefix: "project-alpha"
+ const prefix = await api(
+ "GET",
+ `/v2/rooms?query=${encodeURIComponent('roomId^"project-alpha"')}`
+ );
+ expect(prefix.status).toBe(200);
+ expect(prefix.body.data).toHaveLength(2);
+ const prefixIds = (prefix.body.data as { id: string }[])
+ .map((r) => r.id)
+ .sort();
+ expect(prefixIds).toEqual(["project-alpha-1", "project-alpha-2"]);
+
+ // Filter by metadata: team = "frontend"
+ const meta = await api(
+ "GET",
+ `/v2/rooms?query=${encodeURIComponent('metadata["team"]:"frontend"')}`
+ );
+ expect(meta.status).toBe(200);
+ expect(meta.body.data).toHaveLength(2);
+ const metaIds = (meta.body.data as { id: string }[])
+ .map((r) => r.id)
+ .sort();
+ expect(metaIds).toEqual(["project-alpha-1", "project-beta-1"]);
+
+ // Combined: roomId prefix + metadata
+ const combined = await api(
+ "GET",
+ `/v2/rooms?query=${encodeURIComponent('roomId^"project-alpha" metadata["team"]:"frontend"')}`
+ );
+ expect(combined.status).toBe(200);
+ expect(combined.body.data).toHaveLength(1);
+ expect((combined.body.data as { id: string }[])[0].id).toBe(
+ "project-alpha-1"
+ );
+
+ // Filter by metadata: priority = "low"
+ const low = await api(
+ "GET",
+ `/v2/rooms?query=${encodeURIComponent('metadata["priority"]:"low"')}`
+ );
+ expect(low.status).toBe(200);
+ expect(low.body.data).toHaveLength(2);
+ const lowIds = (low.body.data as { id: string }[]).map((r) => r.id).sort();
+ expect(lowIds).toEqual(["project-alpha-2", "project-beta-1"]);
+ });
+
+ test("get room returns metadata and permissions", async () => {
+ const { status, body } = await api("GET", "/v2/rooms/project-alpha-1");
+ expect(status).toBe(200);
+ expect(body.id).toBe("project-alpha-1");
+ expect(body.metadata).toEqual({
+ team: "frontend",
+ priority: "high",
+ labels: ["bug", "urgent"],
+ });
+ expect(body.defaultAccesses).toEqual(["room:write"]);
+ });
+
+ test("get non-existent room returns 404", async () => {
+ const { status } = await api("GET", "/v2/rooms/does-not-exist");
+ expect(status).toBe(404);
+ });
+
+ test("create duplicate room returns 409", async () => {
+ const { status } = await api("POST", "/v2/rooms", {
+ id: "project-alpha-1",
+ });
+ expect(status).toBe(409);
+ });
+
+ test("update room metadata", async () => {
+ const { status, body } = await api("POST", "/v2/rooms/project-alpha-1", {
+ metadata: { priority: "medium" },
+ });
+ expect(status).toBe(200);
+ // Metadata is merged, not replaced
+ expect(body.metadata).toEqual({
+ team: "frontend",
+ priority: "medium",
+ labels: ["bug", "urgent"],
+ });
+ });
+
+ test("room created without explicit permissions defaults to room:write", async () => {
+ const { status, body } = await api("POST", "/v2/rooms", {
+ id: "default-perms-room",
+ });
+ expect(status).toBe(200);
+ expect(body.defaultAccesses).toEqual(["room:write"]);
+ expect(body.usersAccesses).toEqual({});
+ expect(body.groupsAccesses).toEqual({});
+ });
+
+ test("room created with explicit permissions is reflected in GET", async () => {
+ const { status: createStatus } = await api("POST", "/v2/rooms", {
+ id: "perms-room",
+ defaultAccesses: ["room:read"],
+ usersAccesses: {
+ alice: ["room:write"],
+ bob: ["room:read", "room:presence:write"],
+ },
+ groupsAccesses: {
+ admins: ["room:write"],
+ },
+ });
+ expect(createStatus).toBe(200);
+
+ const { status, body } = await api("GET", "/v2/rooms/perms-room");
+ expect(status).toBe(200);
+ expect(body.defaultAccesses).toEqual(["room:read"]);
+ expect(body.usersAccesses).toEqual({
+ alice: ["room:write"],
+ bob: ["room:read", "room:presence:write"],
+ });
+ expect(body.groupsAccesses).toEqual({
+ admins: ["room:write"],
+ });
+ });
+
+ test("delete room", async () => {
+ const del = await api("DELETE", "/v2/rooms/project-beta-1");
+ expect(del.status).toBe(204);
+
+ // Verify it's gone
+ const get = await api("GET", "/v2/rooms/project-beta-1");
+ expect(get.status).toBe(404);
+
+ // Verify it no longer appears in the list
+ const list = await api("GET", "/v2/rooms");
+ const listIds = (list.body.data as { id: string }[]).map((r) => r.id);
+ expect(listIds).not.toContain("project-beta-1");
+ });
+
+ test("room created without organizationId defaults to 'default'", () => {
+ const room = Rooms.getRoom("project-alpha-1");
+ expect(room).toBeDefined();
+ expect(room!.organizationId).toBe("default");
+ });
+
+ test("create room with explicit organizationId", async () => {
+ const { status } = await api("POST", "/v2/rooms", {
+ id: "org-room-acme",
+ organizationId: "org_acme",
+ metadata: { env: "test" },
+ });
+ expect(status).toBe(200);
+
+ const room = Rooms.getRoom("org-room-acme");
+ expect(room).toBeDefined();
+ expect(room!.organizationId).toBe("org_acme");
+ });
+
+ test("create room with different organizationId", async () => {
+ const { status } = await api("POST", "/v2/rooms", {
+ id: "org-room-globex",
+ organizationId: "org_globex",
+ });
+ expect(status).toBe(200);
+
+ const room = Rooms.getRoom("org-room-globex");
+ expect(room).toBeDefined();
+ expect(room!.organizationId).toBe("org_globex");
+ });
+
+ test("list rooms filtered by organizationId", async () => {
+ const acme = await api("GET", "/v2/rooms?organizationId=org_acme");
+ expect(acme.status).toBe(200);
+ const acmeIds = (acme.body.data as { id: string }[]).map((r) => r.id);
+ expect(acmeIds).toEqual(["org-room-acme"]);
+
+ const globex = await api("GET", "/v2/rooms?organizationId=org_globex");
+ expect(globex.status).toBe(200);
+ const globexIds = (globex.body.data as { id: string }[]).map((r) => r.id);
+ expect(globexIds).toEqual(["org-room-globex"]);
+
+ // Default org rooms (created without explicit organizationId)
+ const defaultOrg = await api("GET", "/v2/rooms?organizationId=default");
+ expect(defaultOrg.status).toBe(200);
+ const defaultIds = (defaultOrg.body.data as { id: string }[]).map(
+ (r) => r.id
+ );
+ expect(defaultIds).not.toContain("org-room-acme");
+ expect(defaultIds).not.toContain("org-room-globex");
+ });
+
+ test("update room with null groupsAccesses removes the group", async () => {
+ // Create a room with group permissions
+ const { status: createStatus } = await api("POST", "/v2/rooms", {
+ id: "null-perms-room",
+ groupsAccesses: { editors: ["room:write"] },
+ usersAccesses: { alice: ["room:write"] },
+ });
+ expect(createStatus).toBe(200);
+
+ // Verify permissions are set
+ const before = await api("GET", "/v2/rooms/null-perms-room");
+ expect(before.body.groupsAccesses).toEqual({ editors: ["room:write"] });
+ expect(before.body.usersAccesses).toEqual({ alice: ["room:write"] });
+
+ // Remove group by setting to null
+ const { status: updateStatus } = await api(
+ "POST",
+ "/v2/rooms/null-perms-room",
+ {
+ groupsAccesses: { editors: null },
+ usersAccesses: { alice: null },
+ }
+ );
+ expect(updateStatus).toBe(200);
+
+ // Verify permissions are removed
+ const after = await api("GET", "/v2/rooms/null-perms-room");
+ expect(after.body.groupsAccesses).toEqual({});
+ expect(after.body.usersAccesses).toEqual({});
+ });
+});