diff --git a/cirro/sdk/project.py b/cirro/sdk/project.py index 3c3b1c2..458bde0 100644 --- a/cirro/sdk/project.py +++ b/cirro/sdk/project.py @@ -288,6 +288,7 @@ def create_sheet(self, sheet: Union[TableSheetInput, ViewSheetInput]) -> DataPor diagnoses = project.create_sheet( TableSheetInput( + sheet_type="TABLE", name="Diagnoses", namespace_name="default", table_name="diagnoses", diff --git a/cirro/sdk/sheet.py b/cirro/sdk/sheet.py index 6ed5c47..4bb86c4 100644 --- a/cirro/sdk/sheet.py +++ b/cirro/sdk/sheet.py @@ -4,8 +4,9 @@ from pandas import DataFrame from cirro_api_client.v1.models import ColumnDef, RowInsert, RowInsertValues, RowUpdate, RowUpdateValues, Sheet, \ - SheetDetail, SheetJob, SheetQueryResponse, SheetType, SqlSortOrder, Status, TableSheetInput, Tag, ViewSheetInput -from cirro_api_client.v1.types import Unset + SheetDataRequest, SheetDetail, SheetJob, SheetQueryResponse, SheetSort, SheetType, SqlSortOrder, Status, \ + TableSheetInput, Tag, ViewSheetInput +from cirro_api_client.v1.types import UNSET, Unset from cirro.cirro_client import CirroApi from cirro.sdk.asset import DataPortalAssets, DataPortalAsset @@ -168,14 +169,13 @@ def to_dataframe(self, Returns: `pandas.DataFrame` """ + sort = SheetSort(order_by=order_by if order_by is not None else UNSET, + order=order if order is not None else UNSET) return query_to_dataframe( - lambda limit, page: self._client.sheets.get_data( + lambda limit, page: self._client.sheets.query_data( project_id=self.project_id, sheet_id=self.id, - limit=limit, - page=page, - order_by=order_by, - order=order + request=SheetDataRequest(limit=limit, page=page, sort=sort) ), max_rows=max_rows ) @@ -277,11 +277,11 @@ def update(self, """ detail = self._get_detail() shared = { + "sheet_type": detail.sheet_type.value, "name": self.name if name is None else name, "namespace_name": detail.namespace_name, "table_name": detail.table_name, "description": self.description if description is None else description, - "audit_read_access": detail.audit_read_access, "tags": detail.tags if tags is None else tags } diff --git a/cirro/services/sheet.py b/cirro/services/sheet.py index 774f12c..58d7013 100644 --- a/cirro/services/sheet.py +++ b/cirro/services/sheet.py @@ -1,13 +1,11 @@ from typing import List from cirro_api_client.v1.api.sheets import get_sheets, get_sheet, create_sheet, update_sheet, delete_sheet, \ - get_sheet_data, query_sheet_data, query_data, insert_sheet_data, update_sheet_data, delete_sheet_data, \ + query_sheet_data, query_data, insert_sheet_data, update_sheet_data, delete_sheet_data, \ refresh_view, trigger_ingest, get_jobs from cirro_api_client.v1.models import Sheet, SheetDetail, TableSheetInput, ViewSheetInput, CreateResponse, \ SheetUpdateResponse, SheetDataRequest, SheetQueryRequest, SheetQueryResponse, SheetDataUpdateResponse, \ - RowInsert, RowUpdate, InsertRowsRequest, UpdateRowsRequest, DeleteRowsRequest, SheetIngestRequest, SheetJob, \ - SqlSortOrder -from cirro_api_client.v1.types import UNSET + RowInsert, RowUpdate, InsertRowsRequest, UpdateRowsRequest, DeleteRowsRequest, SheetIngestRequest, SheetJob from cirro.services.base import BaseService @@ -83,46 +81,21 @@ def delete(self, project_id: str, sheet_id: str) -> None: """ delete_sheet.sync_detailed(project_id=project_id, sheet_id=sheet_id, client=self._api_client) - def get_data(self, - project_id: str, - sheet_id: str, - limit: int = 1000, - page: int = 1, - order_by: str = None, - order: SqlSortOrder = None) -> SheetQueryResponse: + def query_data(self, project_id: str, sheet_id: str, request: SheetDataRequest = None) -> SheetQueryResponse: """ - Retrieve a page of rows from a sheet + Retrieve a page of rows from a sheet, with an optional sort and filter The first column is always `_row_id`, which identifies the row for `update_rows` and `delete_rows`. Args: project_id (str): ID of the Project sheet_id (str): ID of the Sheet - limit (int): Maximum number of rows to return (default 1,000) - page (int): Page to return (default 1) - order_by (str): Column to sort by - order (SqlSortOrder): Sort direction - """ - return get_sheet_data.sync(project_id=project_id, - sheet_id=sheet_id, - limit=limit, - page=page, - order_by=order_by if order_by is not None else UNSET, - order=order if order is not None else UNSET, - client=self._api_client) - - def query_data(self, project_id: str, sheet_id: str, request: SheetDataRequest) -> SheetQueryResponse: - """ - Retrieve a page of rows from a sheet, with an optional sort and filter - - Args: - project_id (str): ID of the Project - sheet_id (str): ID of the Sheet - request (cirro_api_client.v1.models.SheetDataRequest): Page, sort, and filter to apply + request (cirro_api_client.v1.models.SheetDataRequest): Page, sort, and filter to apply; + defaults to the first 1,000 rows, unsorted and unfiltered """ return query_sheet_data.sync(project_id=project_id, sheet_id=sheet_id, - body=request, + body=request if request is not None else SheetDataRequest(), client=self._api_client) def raw_query(self, diff --git a/poetry.lock b/poetry.lock index 7f527ee..3d7a380 100644 --- a/poetry.lock +++ b/poetry.lock @@ -737,14 +737,14 @@ files = [ [[package]] name = "cirro-api-client" -version = "1.7.2" +version = "1.8.0" description = "A client library for accessing Cirro" optional = false python-versions = "<4.0,>=3.11" groups = ["main"] files = [ - {file = "cirro_api_client-1.7.2-py3-none-any.whl", hash = "sha256:a87dd1fdc0bfe11b5214b0b9a11cc14c93a889a0b173cf10ee2d046296b5c275"}, - {file = "cirro_api_client-1.7.2.tar.gz", hash = "sha256:7633f86a4e22beeef49ac4aad67b65a328918726ddda40f9a296f0c6c3da2aca"}, + {file = "cirro_api_client-1.8.0-py3-none-any.whl", hash = "sha256:8c214ee7a3222d1ce9131d6382925619f7f447b421efa2b1e7527f2955eeb9fb"}, + {file = "cirro_api_client-1.8.0.tar.gz", hash = "sha256:9e7b3b48b1071f7b6d9bbf962faabfc6aff9d5ee9af6699b4b790e6d7bd1b017"}, ] [package.dependencies] @@ -4602,4 +4602,4 @@ wdl = ["miniwdl"] [metadata] lock-version = "2.1" python-versions = ">3.11.0,<4.0" -content-hash = "3811a13427a5af94fab8b5f3f99fbbd106a22e7585d43b348b72ea22bf4c10c9" +content-hash = "1e9e4c155dc3d0e8b6250934ce71044e606ca0e3b8c5f4fa798e2a56251bad59" diff --git a/pyproject.toml b/pyproject.toml index 92a5e74..62b4332 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cirro" -version = "1.15.1" +version = "1.16.0" description = "CLI tool and SDK for interacting with the Cirro platform" authors = ["Cirro Bio "] license = "MIT" @@ -11,7 +11,7 @@ packages = [{include = "cirro"}] [tool.poetry.dependencies] python = ">3.11.0,<4.0" attrs = ">=21.3.0" -cirro_api_client = "~1.7.2" +cirro_api_client = "~1.8.0" click = "^8.5.0" boto3 = "~=1.43" questionary = "^2.0.1" diff --git a/samples/Working_with_sheets.ipynb b/samples/Working_with_sheets.ipynb new file mode 100644 index 0000000..e3dabc8 --- /dev/null +++ b/samples/Working_with_sheets.ipynb @@ -0,0 +1,516 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0f7a31b4", + "metadata": {}, + "source": [ + "## Working with sheets\n", + "\n", + "Sheets are tables of structured data held alongside a project. This notebook follows one sheet from start to finish: it creates a table of sample metadata, adds the samples, reads and summarizes them, corrects a few rows, and then deletes the sheet." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "82aaadbb", + "metadata": {}, + "outputs": [], + "source": [ + "from cirro import DataPortal\n", + "\n", + "portal = DataPortal()\n", + "project = portal.get_project_by_name(\"Test project 1\")" + ] + }, + { + "cell_type": "markdown", + "id": "4371a4bd", + "metadata": {}, + "source": [ + "#### Creating a sheet\n", + "\n", + "Each column has a name and a data type. `sample_id` is `required`, so it cannot be null, and `unique`, so no two rows can share it.\n", + "\n", + "`sheet_type` says whether the sheet is a `TABLE`, which holds rows you write to it, or a `VIEW`, which is materialized from a query. `table_name` is how SQL queries refer to the sheet, and `namespace_name` groups related tables and cannot be changed later." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "df774c3e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Name: Example Samples\n", + "Id: 788a504e-0484-49d6-9d9a-98a11e0c584a\n", + "Description: Sample metadata used by the Sheets example\n", + "Sheet Type: TABLE\n", + "Total Row Count: 0\n" + ] + } + ], + "source": [ + "from cirro_api_client.v1.models import ColumnDataType, ColumnDef, SheetCreationMode, TableSheetInput\n", + "\n", + "sheet = project.create_sheet(\n", + " TableSheetInput(\n", + " sheet_type=\"TABLE\",\n", + " name=\"Example Samples\",\n", + " description=\"Sample metadata used by the Sheets example\",\n", + " namespace_name=\"default\",\n", + " table_name=\"example_samples\",\n", + " sheet_creation_mode=SheetCreationMode.STANDARD,\n", + " columns=[\n", + " ColumnDef(name=\"sample_id\", data_type=ColumnDataType.STRING, required=True, unique=True),\n", + " ColumnDef(name=\"tissue\", data_type=ColumnDataType.STRING),\n", + " ColumnDef(name=\"condition\", data_type=ColumnDataType.STRING),\n", + " ColumnDef(name=\"read_count\", data_type=ColumnDataType.BIGINT)\n", + " ]\n", + " )\n", + ")\n", + "print(sheet)" + ] + }, + { + "cell_type": "markdown", + "id": "511771f8", + "metadata": {}, + "source": [ + "#### Inserting rows\n", + "\n", + "Each row maps column names to values, and columns left out are null. S3's read count isn't in yet, so it is left out. To insert from a DataFrame, pass `df.to_dict(orient=\"records\")`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ed7f5023", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Inserted 5 rows\n" + ] + } + ], + "source": [ + "inserted = sheet.insert_rows([\n", + " {\"sample_id\": \"S1\", \"tissue\": \"liver\", \"condition\": \"control\", \"read_count\": 21000000},\n", + " {\"sample_id\": \"S2\", \"tissue\": \"liver\", \"condition\": \"treated\", \"read_count\": 19500000},\n", + " {\"sample_id\": \"S3\", \"tissue\": \"lung\", \"condition\": \"control\"},\n", + " {\"sample_id\": \"S4\", \"tissue\": \"kidney\", \"condition\": \"treated\", \"read_count\": 18000000},\n", + " {\"sample_id\": \"S5\", \"tissue\": \"lung\", \"condition\": \"treated\", \"read_count\": 22500000}\n", + "])\n", + "print(f\"Inserted {inserted} rows\")" + ] + }, + { + "cell_type": "markdown", + "id": "e69dd763", + "metadata": {}, + "source": [ + "#### Reading the sheet\n", + "\n", + "`to_dataframe` reads the whole sheet. Its first column, `_row_id`, identifies each row, and is how the updates and deletes below pick out the rows to change." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "df662b09", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_row_idsample_idtissueconditionread_countsys_created_atsys_updated_at
00S1livercontrol21000000.02026-10-23T20:47:38.903285Z2026-10-23T20:47:38.903285Z
11S2livertreated19500000.02026-10-23T20:47:38.903285Z2026-10-23T20:47:38.903285Z
22S3lungcontrolNaN2026-10-23T20:47:38.903285Z2026-10-23T20:47:38.903285Z
33S4kidneytreated18000000.02026-10-23T20:47:38.903285Z2026-10-23T20:47:38.903285Z
44S5lungtreated22500000.02026-10-23T20:47:38.903285Z2026-10-23T20:47:38.903285Z
\n", + "
" + ], + "text/plain": [ + " _row_id sample_id tissue condition read_count \\\n", + "0 0 S1 liver control 21000000.0 \n", + "1 1 S2 liver treated 19500000.0 \n", + "2 2 S3 lung control NaN \n", + "3 3 S4 kidney treated 18000000.0 \n", + "4 4 S5 lung treated 22500000.0 \n", + "\n", + " sys_created_at sys_updated_at \n", + "0 2026-10-23T20:47:38.903285Z 2026-10-23T20:47:38.903285Z \n", + "1 2026-10-23T20:47:38.903285Z 2026-10-23T20:47:38.903285Z \n", + "2 2026-10-23T20:47:38.903285Z 2026-10-23T20:47:38.903285Z \n", + "3 2026-10-23T20:47:38.903285Z 2026-10-23T20:47:38.903285Z \n", + "4 2026-10-23T20:47:38.903285Z 2026-10-23T20:47:38.903285Z " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = sheet.to_dataframe()\n", + "row_ids = {sample_id: int(row_id) for sample_id, row_id in zip(df[\"sample_id\"], df[\"_row_id\"])}\n", + "df" + ] + }, + { + "cell_type": "markdown", + "id": "704200ab", + "metadata": {}, + "source": [ + "#### Querying with SQL\n", + "\n", + "`query_sheets` runs SQL across the project's sheets and returns a DataFrame. Refer to a sheet by its `table_name`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "9e6e1cd9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
conditionsamplesmean_reads
0treated320000000.0
1control221000000.0
\n", + "
" + ], + "text/plain": [ + " condition samples mean_reads\n", + "0 treated 3 20000000.0\n", + "1 control 2 21000000.0" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "project.query_sheets(\"\"\"\n", + " SELECT condition, COUNT(*) AS samples, AVG(read_count) AS mean_reads\n", + " FROM example_samples\n", + " GROUP BY condition\n", + "\"\"\")" + ] + }, + { + "cell_type": "markdown", + "id": "18ef34e7", + "metadata": {}, + "source": [ + "#### Updating a row\n", + "\n", + "S3's read count is in now. Only the columns you name are written; the rest of the row is left alone." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "339c820f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sheet.update_rows({row_ids[\"S3\"]: {\"read_count\": 20100000}})" + ] + }, + { + "cell_type": "markdown", + "id": "217672d9", + "metadata": {}, + "source": [ + "#### Deleting a row\n", + "\n", + "S5 failed QC, so remove it." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0bacd687", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_row_idsample_idtissueconditionread_countsys_created_atsys_updated_at
00S1livercontrol210000002026-10-23T20:47:38.903285Z2026-10-23T20:47:38.903285Z
11S2livertreated195000002026-10-23T20:47:38.903285Z2026-10-23T20:47:38.903285Z
23S4kidneytreated180000002026-10-23T20:47:38.903285Z2026-10-23T20:47:38.903285Z
32S3lungcontrol201000002026-10-23T20:47:38.903285Z2026-10-23T20:48:11.223366Z
\n", + "
" + ], + "text/plain": [ + " _row_id sample_id tissue condition read_count \\\n", + "0 0 S1 liver control 21000000 \n", + "1 1 S2 liver treated 19500000 \n", + "2 3 S4 kidney treated 18000000 \n", + "3 2 S3 lung control 20100000 \n", + "\n", + " sys_created_at sys_updated_at \n", + "0 2026-10-23T20:47:38.903285Z 2026-10-23T20:47:38.903285Z \n", + "1 2026-10-23T20:47:38.903285Z 2026-10-23T20:47:38.903285Z \n", + "2 2026-10-23T20:47:38.903285Z 2026-10-23T20:47:38.903285Z \n", + "3 2026-10-23T20:47:38.903285Z 2026-10-23T20:48:11.223366Z " + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sheet.delete_rows([row_ids[\"S5\"]])\n", + "\n", + "sheet.to_dataframe()" + ] + }, + { + "cell_type": "markdown", + "id": "a0ea9149", + "metadata": {}, + "source": [ + "#### Deleting the sheet\n", + "\n", + "Delete the sheet, along with its rows, once you are done with it." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "3db31738", + "metadata": {}, + "outputs": [], + "source": [ + "sheet.delete()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09366127", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cirro-sdk", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.17" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/test_sheets.py b/tests/test_sheets.py index d5a3021..5b87ecd 100644 --- a/tests/test_sheets.py +++ b/tests/test_sheets.py @@ -36,7 +36,8 @@ "updatedAt": "2026-01-01T00:00:00Z", "totalRowCount": 0, "tags": [], - "columnRelationships": [] + "columnRelationships": [], + "mappingTargets": [] } _COLUMNS = [ @@ -52,7 +53,7 @@ def _make_sheet(client: Mock, **overrides) -> DataPortalSheet: def _paging_client(total_rows: int): """ - Mock client whose sheets.get_data pages the way the API does, reading the + Mock client whose sheets.query_data pages the way the API does, reading the slice at offset (page - 1) * limit. Returns the client alongside the list of (page, limit) pairs it was asked @@ -60,14 +61,14 @@ def _paging_client(total_rows: int): """ requests = [] - def get_data(project_id, sheet_id, limit, page, order_by, order): - requests.append((page, limit)) - start = (page - 1) * limit - rows = [[i, f"code-{i}"] for i in range(start, min(start + limit, total_rows))] + def query_data(project_id, sheet_id, request): + requests.append((request.page, request.limit)) + start = (request.page - 1) * request.limit + rows = [[i, f"code-{i}"] for i in range(start, min(start + request.limit, total_rows))] return SheetQueryResponse(columns=_COLUMNS, rows=rows, total_row_count=total_rows) client = Mock() - client.sheets.get_data.side_effect = get_data + client.sheets.query_data.side_effect = query_data return client, requests @@ -141,9 +142,9 @@ def test_sort_is_passed_through(self): client, _ = _paging_client(10) _make_sheet(client).to_dataframe(order_by="icd_code", order=SqlSortOrder.DESC) - _, kwargs = client.sheets.get_data.call_args - self.assertEqual("icd_code", kwargs["order_by"]) - self.assertEqual(SqlSortOrder.DESC, kwargs["order"]) + _, kwargs = client.sheets.query_data.call_args + self.assertEqual("icd_code", kwargs["request"].sort.order_by) + self.assertEqual(SqlSortOrder.DESC, kwargs["request"].sort.order) class NamespaceQueryTest(unittest.TestCase): @@ -192,7 +193,7 @@ class CreateSheetTest(unittest.TestCase): def test_create_sheet_returns_the_new_sheet(self): client = Mock() client.sheets.create.return_value = CreateResponse(id="sheet-1", message="created") - client.sheets.get.return_value = SheetDetail.from_dict({**_SHEET, "auditReadAccess": False}) + client.sheets.get.return_value = SheetDetail.from_dict(_SHEET) project = DataPortalProject(Project.from_dict(_PROJECT), client) sheet_input = TableSheetInput( @@ -258,7 +259,6 @@ def _sheet_with_detail(**detail_overrides): client = Mock() client.sheets.get.return_value = SheetDetail.from_dict({ **_SHEET, - "auditReadAccess": False, "columns": [ColumnDef(name="icd_code", data_type=ColumnDataType.STRING).to_dict()], **detail_overrides }) @@ -273,6 +273,8 @@ def test_update_sends_the_whole_table_state(self): _, kwargs = client.sheets.update.call_args target = kwargs["sheet"] self.assertIsInstance(target, TableSheetInput) + # the API tells a table from a view by sheetType, so it must be sent + self.assertEqual("TABLE", target.sheet_type) self.assertEqual("Renamed", target.name) # untouched fields are carried over rather than dropped self.assertEqual("ICD codes", target.description) @@ -288,13 +290,14 @@ def test_update_sends_the_whole_view_state(self): _, kwargs = client.sheets.update.call_args self.assertIsInstance(kwargs["sheet"], ViewSheetInput) + self.assertEqual("VIEW", kwargs["sheet"].sheet_type) self.assertEqual("Rolled up", kwargs["sheet"].description) self.assertEqual("Diagnoses", kwargs["sheet"].name) def test_update_reflects_the_rename_locally(self): sheet, client = self._sheet_with_detail() client.sheets.update.return_value = SheetUpdateResponse( - sheet=SheetDetail.from_dict({**_SHEET, "name": "Renamed", "auditReadAccess": False}) + sheet=SheetDetail.from_dict({**_SHEET, "name": "Renamed"}) ) sheet.update(name="Renamed") @@ -336,7 +339,6 @@ def test_columns_fetches_the_sheet_detail(self): client = Mock() client.sheets.get.return_value = SheetDetail.from_dict({ **_SHEET, - "auditReadAccess": False, "columns": [ColumnDef(name="icd_code", data_type=ColumnDataType.STRING).to_dict()] }) sheet = _make_sheet(client)