API reference.

Pygrister: a Python client for the Grist API.

Grist is a relational spreadsheet with tons of batteries included. The Grist API allows you to programmatically retrieve/update your data stored on Grist, and manipulate most of the basic Grist objects, such as workspaces, documents, user permissions and so on.

Pygrister is a basic Grist client, wrapping all the documented APIs. Pygrister keeps track of some configuration for you, remembering your team site, workspace, working document, so that you don’t have to type in the boring stuff every time. Apart from that and little else, Pygrister is rather low-level: it will call the api and retrieve the response “as is”. If the api call is malformed, you will simply receive a bad HTTP status code.

Pygrister will not attempt to convert sent and received data types: however, it will execute custom converter functions, if provided.

Basic usage goes as follows:

from pygrister.api import GristApi

grist = GristApi()
# list users/permissions for the current document
status_code, response = grist.list_doc_users()
# fetch all rows in a table
status_code, response = grist.see_records('Table1') 
# add a column to a table
cols = [{'id': 'age', 'fields': {'label':'age', 'type': 'Int'}}]
status_code, response = grist.add_cols('Table1', cols) 

You should read the documentation first, to learn about the basic Pygrister concepts, patterns and configurations. However, the api call functions themselves are not documented in Pygrister: see the Grist API reference documentation for details about each api signature, and browse the Pygrister test suite for more usage examples.

pygrister.api.MAXSAVEDRESP = 5000

max length of resp. content, saved for inspection

pygrister.api.SAVEBINARYRESP = False

if binary resp. content should be saved for inspection

pygrister.api.get_config() dict[str, str]

the global, “static” configuration

pygrister.api.check_safemode(funct)

If Pygrister is in safemode, no writing API call will pass through.

pygrister.api.Apiresp

the return type of all api call functions

alias of tuple[int, Any]

class pygrister.api.Paginator(provider, start, items, lenname, res_transform, **query_args)

A simple iterable object to wrap Api calls that needs pagination.

The main GristApi class.

class pygrister.api.GristApi(config: dict[str, str] | None = None, in_converter: dict | None = None, out_converter: dict | None = None, request_options: dict | None = None, custom_configurator: Configurator | None = None)
apicalls: int

total number of API calls

ok: bool

if an HTTPError occurred

req_url: str

last request url

req_body: str

last request body

req_headers: dict

last request headers

req_method: str

last request method

resp_content: str | bytes

last response content

resp_code: str

last response status code

resp_reason: str

last response status reason

resp_headers: dict

last reponse headers

session

Requests session object, or None

in_converter

converters for input data

out_converter

converters for output data

request_options

other options to pass to request

reconfig(config: dict[str, str] | None = None) None

Reload the configuration options.

A shortcut for self.configurator.recongif(config).

update_config(config: dict[str, str]) None

Edit the configuration options.

A shortcut for self.configurator.update_config(config).

make_server(team_name: str = '') str

Construct the “server” part of the API url, up to “/api”.

A shortcut for self.configurator.make_server(team_name).

open_session() None

Open a Requests sessions for all subsequent Api calls.

close_session() None

Close an open session, if any.

apicall(url: str, method: str = 'GET', headers: dict | None = None, params: dict | None = None, json: dict | None = None, filename: str = '', upload_files: list | None = None) tuple[int, Any]

The engine responsible for actually calling the Apis.

inspect() str

Collect info on the last api call that was responded to by the server.

Intended for debug: add a print(self.inspect()) right after the call to inspect. Works even if the server returned a “bad” status code (aka HTTPError). Does not work if the call itself was not successful (eg., timed out).

see_user(user_id: int) tuple[int, Any]

Implement GET /scim/v2/Users/{userId}.

If successful, response will be a dict of user details. If scim is not enabled, will return Http 501.

see_myself() tuple[int, Any]

Implement GET /scim/v2/Me.

If successful, response will be a dict of logged-in user’s details. If scim is not enabled, will return Http 501.

list_users(start: int = 1, chunk: int = 10, filter: str = '') Paginator

Implement GET /scim/v2/Users.

This is a paginated api: return an iterable object which, in turn, will retrieve chunk users at a time, as a list[dict].

list_users_raw(start: int = 1, chunk: int = 10, filter: str = '') tuple[int, Any]

Implement GET /scim/v2/Users.

If successful, response will be a dict of user data. If scim is not enabled, will return Http 501.

add_user(username: str, emails: list[str], formatted_name: str = '', display_name: str = '', lang: str = 'en', locale: str = 'en', photos: list[str] | None = None, schemas: list[str] | None = None) tuple[int, Any]

Implement POST /scim/v2/Users.

Note: schemas defaults to ['urn:ietf:params:scim:schemas:core:2.0:User']

If successful, response will be the user id as an int. If scim is not enabled, will return Http 501.

update_user_override(user_id: int, username: str, emails: list[str], formatted_name: str = '', display_name: str = '', lang: str = 'en', locale: str = 'en', photos: list[str] | None = None, schemas: list[str] | None = None) tuple[int, Any]

Implement PUT /scim/v2/Users/{userId}.

Note: schemas defaults to ['urn:ietf:params:scim:schemas:core:2.0:User']

If successful, response will be None. If scim is not enabled, will return Http 501.

update_user(user_id: int, operations: list[dict], schemas: list[str] | None = None) tuple[int, Any]

Implement PATCH /scim/v2/Users/{userId}.

Note: schemas defaults to ['urn:ietf:params:scim:api:messages:2.0:PatchOp']

If successful, response will be None. If scim is not enabled, will return Http 501.

delete_user(user_id: int)

Implement DELETE /scim/v2/Users/{userId}.

If successful, response will be None. If scim is not enabled, will return Http 501.

delete_myself(user: str, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement DELETE /users/{userId}.

Note: since this is the only /users endpoint implemented by Grist right now, and since it is of little help (you can only delete your own account), we choose not to implement this one, and leave it here as a stub.

search_users(start: int = 1, chunk: int = 10, sort: str = '', asc: bool = True, filter: str = '', attrib: list[str] | None = None, no_attrib: list[str] | None = None, schemas: list[str] | None = None) Paginator

Implement POST /scim/v2/Users/.search.

Note: schemas defaults to ['urn:ietf:params:scim:api:messages:2.0:BulkRequest']

This is a paginated api: return an iterable object which, in turn, will retrieve chunk users at a time, as a list[dict].

search_users_raw(start: int = 1, chunk: int = 10, sort: str = '', asc: bool = True, filter: str = '', attrib: list[str] | None = None, no_attrib: list[str] | None = None, schemas: list[str] | None = None) tuple[int, Any]

Implement POST /scim/v2/Users/.search.

Note: schemas defaults to ['urn:ietf:params:scim:api:messages:2.0:BulkRequest']

If successful, response will be a dict of user data. If scim is not enabled, will return Http 501.

bulk_users(operations: list[dict], schemas: list[str] | None = None) tuple[int, Any]

Implement POST /scim/v2/Bulk.

Note: schemas defaults to ['urn:ietf:params:scim:api:messages:2.0:BulkRequest']

If successful, response will be a list[int] of status codes for each operation (inspect GristApi.resp_content for details, if any operations resulted in a bad status code). If scim is not enabled, will return Http 501.

see_scim_schemas() tuple[int, Any]

Implement GET /scim/v2/Schemas.

If successful, response will a dict of scim schemas. If scim is not enabled, will return Http 501.

see_scim_config() tuple[int, Any]

Implement GET /scim/v2/ServiceProviderConfig.

If successful, response will a dict of scim provider configuration. If scim is not enabled, will return Http 501.

see_scim_resources() tuple[int, Any]

Implement GET /scim/v2/ResourceTypes.

If successful, response will a dict of scim resources. If scim is not enabled, will return Http 501.

list_team_sites() tuple[int, Any]

Implement GET /orgs.

If successful, response will be a list[dict] of site details.

see_team(team_id: str = '') tuple[int, Any]

Implement GET /orgs/{orgId}.

If successful, response will be a dict of site details.

update_team(new_name: str, team_id: str = '') tuple[int, Any]

Implement PATCH /orgs/{orgId}.

If successful, response will be None. Note that renaming a team will not change the subdomain too!

delete_team(team_id: str = '') tuple[int, Any]

Implement DELETE /orgs/{orgId}.

If successful, response will be None.

list_team_users(team_id: str = '') tuple[int, Any]

Implement GET /orgs/{orgId}/access.

If successful, response will be a list[dict] of users.

update_team_users(users: dict[str, str], team_id: str = '') tuple[int, Any]

Implement PATCH /orgs/{orgId}/access.

If successful, response will be None.

list_workspaces(team_id: str = '') tuple[int, Any]

Implement GET /{orgId}/workspaces.

If successful, response will be a list[dict] of workspaces.

add_workspace(name: str, team_id: str = '') tuple[int, Any]

Implement POST /{orgId}/workspaces.

If successful, response will be the workspace id as an int.

see_workspace(ws_id: int = 0) tuple[int, Any]

Implement GET /workspaces/{workspaceId}.

If successful, response will be a dict of workspace details.

update_workspace(new_name: str, ws_id: int = 0) tuple[int, Any]

Implement PATCH /workspaces/{workspaceId}.

If successful, response will be None.

delete_workspace(ws_id: int = 0) tuple[int, Any]

Implement DELETE /workspaces/{workspaceId}.

If successful, response will be None.

list_workspace_users(ws_id: int = 0) tuple[int, Any]

Implement GET /workspaces/{workspaceId}/access.

If successful, response will be a list[dict] of users.

update_workspace_users(users: dict[str, str], ws_id: int = 0) tuple[int, Any]

Implement PATCH /workspaces/{workspaceId}/access.

If successful, response will be None.

add_doc(name: str, pinned: bool = False, ws_id: int = 0) tuple[int, Any]

Implement POST /workspaces/{workspaceId}/docs.

If successful, response will be the doc id as a str.

see_doc(doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}.

If successful, response will be a dict of doc details.

update_doc(new_name: str = '', pinned: bool = False, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement PATCH /docs/{docId}.

If successful, response will be None.

delete_doc(doc_id: str, team_id: str = '') tuple[int, Any]

Implement DELETE /docs/{docId}.

If successful, response will be None.

delete_doc_history(keep: int = 0, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/states/remove.

If successful, response will be None.

move_doc(ws_id: int, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement PATCH /docs/{docId}/move.

If successful, response will be None.

reload_doc(doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/force-reload.

If successful, response will be None.

list_doc_users(doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/access.

If successful, response will be a list[dict] of users.

update_doc_users(users: dict[str, str], max: str = 'owners', doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement PATCH /docs/{docId}/access.

If successful, response will be None.

download_sqlite(filename: str, nohistory: bool = False, template: bool = False, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/download.

If successful, response will be None.

download_excel(filename: str, table_id: str, header: str = 'label', doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/download/xlsx.

If successful, response will be None.

download_csv(filename: str, table_id: str, header: str = 'label', doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/download/csv.

If successful, response will be None.

download_schema(table_id: str, header: str = 'label', filename: str = '', doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/download/table-schema.

If successful, schema will be returned as json; pass the filename param to have it downloaded as a json file instead.

list_records(table_id: str, filter: dict | None = None, sort: str = '', limit: int = 0, hidden: bool = False, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/tables/{tableId}/records.

If a converter is found for this table, data conversion will be attempted. If successful, response will be a list of “Pygrister records with id” (see docs).

add_records(table_id: str, records: list[dict], noparse: bool = False, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/tables/{tableId}/records.

records: a list of “Pygrister records without id” (see docs). If a converter is found for this table, data conversion will be attempted. If successful, response will be a list[int] of added record ids.

update_records(table_id: str, records: list[dict], noparse: bool = False, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement PATCH /docs/{docId}/tables/{tableId}/records.

records: a list of “Pygrister records with id” (see docs). If a converter is found for this table, data conversion will be attempted. If successful, response will be None.

add_update_records(table_id: str, records: list[dict], noparse: bool = False, onmany: str = 'first', noadd: bool = False, noupdate: bool = False, allow_empty_require: bool = False, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement PUT /docs/{docId}/tables/{tableId}/records.

If a converter is found for this table, data conversion will be attempted. If successful, response will be None.

list_tables(doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/tables.

If successful, response will be a list[dict] of tables.

add_tables(tables: list[dict], doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/tables.

If successful, response will be a list[str] of added table ids.

update_tables(tables: list[dict], doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement PATCH /docs/{docId}/tables.

If successful, response will be None.

list_cols(table_id: str, hidden: bool = False, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/tables/{tableId}/columns.

If successful, response will be a list[dict] of columns.

add_cols(table_id: str, cols: list[dict], doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/tables/{tableId}/columns.

If successful, response will be a list[str] of added col ids.

update_cols(table_id: str, cols: list[dict], doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement PATCH /docs/{docId}/tables/{tableId}/columns.

If successful, response will be None.

add_update_cols(table_id: str, cols: list[dict], noadd: bool = True, noupdate: bool = True, replaceall: bool = False, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement PUT /docs/{docId}/tables/{tableId}/columns.

If successful, response will be None.

delete_column(table_id: str, col_id: str, doc_id: str, team_id: str = '') tuple[int, Any]

Implement DELETE /docs/{docId}/tables/{tableId}/columns/{colId}.

If successful, response will be None.

delete_rows(table_id: str, rows: list[int], doc_id: str, team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/tables/{tableId}/data/delete.

If successful, response will be None.

list_attachments(filter: dict | None = None, sort: str = '', limit: int = 0, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/attachments.

If successful, response will be a list[dict] of attachments.

upload_attachment(filename: str, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/attachments for one file only.

This is deprecated and redirects to upload_attachments which you should use instead.

upload_attachments(filenames: list, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/attachments.

If successful, response will be a list[int] of attachments ids.

upload_restore_attachments(filename: str, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/attachments/archive.

filename: must be a file name without extension of a tar file. If successful, response will be a summary dict of attachments used.

see_attachment(attachment_id: int, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/attachments/{attachmentId}.

If successful, response will be a dict of attachment metadata.

download_attachment(filename: str, attachment_id: int, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/attachments/{attachmentId}/download.

If successful, response will be None.

download_attachments(filename: str = '', format: str = 'tar', doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/attachments/archive.

filename: must be a file name without extension and defaults to doc_<doc_id>_attachments.<format>. If successful, response will be None.

see_attachment_store(doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/attachments/store.

If successful, response will be either external or internal.

update_attachment_store(internal_store: bool = True, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/attachments/store.

Pass internal_store=True to set internal storage, False to set external storage. If successful, response will be None. Passing False will return Http 400 if external storage is not configured.

list_store_settings(doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/attachments/stores.

If successful, response will be a list of external storage settings.

transfer_attachments(doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/attachments/transferAll.

If successful, response will be a dict of transfer details. If external storage is not configured, will return Http 404.

get_transfer_status(doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/attachments/transferStatus.

If successful, response will be a dict of transfer details. If external storage is not configured, will return Http 404.

list_webhooks(doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/webhooks.

If successful, response will be a list[dict] of webhooks.

add_webhooks(webhooks: list[dict], doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/webhooks.

If successful, response will be a list[str] of added webhook ids.

update_webhook(webhook_id: str, webhook: dict, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement PATCH /docs/{docId}/webhooks/{webhookId}.

If successful, response will be None.

delete_webhook(webhook_id: str, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement DELETE /docs/{docId}/webhooks/{webhookId}.

If successful, response will be None.

empty_payloads_queue(doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement DELETE /docs/{docId}/webhooks/queue.

If successful, response will be None.

run_sql(sql: str, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement GET /docs/{docId}/sql.

If a converter named “sql” is found, data conversion will be attempted. If successful, response will be a list of “Pygrister records” (see docs) with or without id, depending on the query.

run_sql_with_args(sql: str, qargs: list, timeout: int = 1000, doc_id: str = '', team_id: str = '') tuple[int, Any]

Implement POST /docs/{docId}/sql.

If a converter named “sql” is found, data conversion will be attempted. If successful, response will be a list of “Pygrister records” (see docs) with or without id, depending on the query.

Pygrister exceptions.

Pygrister exception hierarchy.

Exceptions listed here can be raised by Pygrister, and they concern its internal functioning. Note that Grist API calls themselves, however, are delegated to Requests, and will (much more frequently!) throw the exceptions provided by Requests.

exception pygrister.exceptions.GristApiException

The base GristApi exception.

exception pygrister.exceptions.GristApiNotConfigured

A configuration error occurred.

exception pygrister.exceptions.GristApiNotImplemented

This API is not yet implemented by Pygrister.

exception pygrister.exceptions.GristApiInSafeMode

Pygrister is in safe mode, no writing to the db is possible.