Repository: cognitedata/cognite-sdk-python
Pull request #2157: [Functions] fix moar typing issues
Author: login: dbrattli
Labels: name: risk-review-ongoing
description: Risk review is in progress
name: waiting-for-team
description: Waiting for the submitter or reviewer of the PR to take an action
Created: 2025-05-21T12:09:03Z | Merged: 2025-05-23T08:34:53Z

## Description

Fix typing issues with Functions API

## Checklist:
- [ ] ~~Tests added/updated.~~
- [x] Documentation updated. Documentation is generated from docstrings - these must be updated according to your change.
  If a new method has been added it should be referenced in [cognite.rst](https://github.com/cognitedata/cognite-sdk-python/blob/master/docs/source/cognite.rst) in order to generate docs based on its docstring.
- [x] Changelog updated in [CHANGELOG.md](https://github.com/cognitedata/cognite-sdk-python/blob/master/CHANGELOG.md).
- [ ] Version bumped. If triggering a new release is desired, bump the version number in [_version.py](https://github.com/cognitedata/cognite-sdk-python/blob/master/cognite/client/_version.py) and [pyproject.toml](https://github.com/cognitedata/cognite-sdk-python/blob/master/pyproject.toml) per [semantic versioning](https://semver.org/).



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb50fd87f6..8cd63c4eba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,10 @@ Changes are grouped as follows
 - `Fixed` for any bug fixes.
 - `Security` in case of vulnerabilities.
 
+## [Unreleased]
+### Fixed
+- Fixes type annotations for Functions API. Adds new `FunctionHandle` type for annotating function handles.
+
 ## [7.75.1] - 2025-05-15
 ### Fixed
 - Fixes missing `instance_id` field in `Document` class returned from `client.documents.list()` and `client.documents.search()`.
diff --git a/cognite/client/_api/functions.py b/cognite/client/_api/functions.py
index fabf7c43a7..992b577ce3 100644
--- a/cognite/client/_api/functions.py
+++ b/cognite/client/_api/functions.py
@@ -12,7 +12,7 @@
 from multiprocessing import Process, Queue
 from pathlib import Path
 from tempfile import TemporaryDirectory
-from typing import TYPE_CHECKING, Any, Literal, NoReturn, cast, overload
+from typing import TYPE_CHECKING, Literal, NoReturn, cast, overload
 from zipfile import ZipFile
 
 from cognite.client._api_client import APIClient
@@ -34,13 +34,18 @@
 from cognite.client.data_classes.functions import (
     HANDLER_FILE_NAME,
     FunctionCallsFilter,
+    FunctionHandle,
     FunctionScheduleWrite,
     FunctionsStatus,
     FunctionStatus,
     FunctionWrite,
     RunTime,
 )
-from cognite.client.utils._auxiliary import at_most_one_is_not_none, is_unlimited, split_into_chunks
+from cognite.client.utils._auxiliary import (
+    at_most_one_is_not_none,
+    is_unlimited,
+    split_into_chunks,
+)
 from cognite.client.utils._identifier import Identifier, IdentifierSequence
 from cognite.client.utils._importing import local_import
 from cognite.client.utils._session import create_session_and_return_nonce
@@ -98,7 +103,12 @@ class FunctionsAPI(APIClient):
     _RESOURCE_PATH = "/functions"
     _RESOURCE_PATH_CALL = "/functions/{}/call"
 
-    def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None:
+    def __init__(
+        self,
+        config: ClientConfig,
+        api_version: str | None,
+        cognite_client: CogniteClient,
+    ) -> None:
         super().__init__(config, api_version, cognite_client)
         self.calls = FunctionCallsAPI(config, api_version, cognite_client)
         self.schedules = FunctionSchedulesAPI(config, api_version, cognite_client)
@@ -190,7 +200,7 @@ def create(
         folder: str | None = None,
         file_id: int | None = None,
         function_path: str = HANDLER_FILE_NAME,
-        function_handle: Callable | None = None,
+        function_handle: FunctionHandle | None = None,
         external_id: str | None = None,
         description: str | None = None,
         owner: str | None = None,
@@ -227,7 +237,7 @@ def create(
             folder (str | None): Path to the folder where the function source code is located.
             file_id (int | None): File ID of the code uploaded to the Files API.
             function_path (str): Relative path from the root folder to the file containing the `handle` function. Defaults to `handler.py`. Must be on POSIX path format.
-            function_handle (Callable | None): Reference to a function object, which must be named `handle`.
+            function_handle (FunctionHandle | None): Reference to a function object, which must be named `handle`.
             external_id (str | None): External id of the function.
             description (str | None): Description of the function.
             owner (str | None): Owner of this function. Typically used to know who created it.
@@ -315,7 +325,7 @@ def _create_function_obj(
         folder: str | None = None,
         file_id: int | None = None,
         function_path: str = HANDLER_FILE_NAME,
-        function_handle: Callable | None = None,
+        function_handle: FunctionHandle | None = None,
         external_id: str | None = None,
         description: str | None = None,
         owner: str | None = None,
@@ -370,7 +380,9 @@ def _create_function_obj(
         return function
 
     def delete(
-        self, id: int | Sequence[int] | None = None, external_id: str | SequenceNotStr[str] | None = None
+        self,
+        id: int | Sequence[int] | None = None,
+        external_id: str | SequenceNotStr[str] | None = None,
     ) -> None:
         """`Delete one or more functions. <https://developer.cognite.com/api#tag/Functions/operation/deleteFunctions>`_
 
@@ -386,7 +398,10 @@ def delete(
                 >>> client = CogniteClient()
                 >>> client.functions.delete(id=[1,2,3], external_id="function3")
         """
-        self._delete_multiple(identifiers=IdentifierSequence.load(ids=id, external_ids=external_id), wrap_ids=True)
+        self._delete_multiple(
+            identifiers=IdentifierSequence.load(ids=id, external_ids=external_id),
+            wrap_ids=True,
+        )
 
     def list(
         self,
@@ -438,7 +453,10 @@ def list(
 
         # The _list method is not used as the /list endpoint does not
         # respond with a cursor (pagination is not supported)
-        res = self._post(url_path=f"{self._RESOURCE_PATH}/list", json={"filter": filter, "limit": limit})
+        res = self._post(
+            url_path=f"{self._RESOURCE_PATH}/list",
+            json={"filter": filter, "limit": limit},
+        )
 
         return FunctionList._load(res.json()["items"], cognite_client=self._cognite_client)
 
@@ -642,9 +660,15 @@ def _zip_and_upload_handle(
 
     @staticmethod
     def _assert_exactly_one_of_folder_or_file_id_or_function_handle(
-        folder: str | None, file_id: int | None, function_handle: Callable[..., Any] | None
+        folder: str | None,
+        file_id: int | None,
+        function_handle: FunctionHandle | None,
     ) -> None:
-        source_code_options = {"folder": folder, "file_id": file_id, "function_handle": function_handle}
+        source_code_options = {
+            "folder": folder,
+            "file_id": file_id,
+            "function_handle": function_handle,
+        }
         # TODO: Fix to use exactly_one_is_not_none function
         given_source_code_options = [key for key in source_code_options if source_code_options[key]]
         if not given_source_code_options:
@@ -766,7 +790,9 @@ def validate_function_folder(root_path: str, function_path: str, skip_folder_val
             _run_import_check_backup(root_path, module_path)
 
 
-def _validate_function_handle(handle_obj: Callable | ast.FunctionDef) -> None:
+def _validate_function_handle(
+    handle_obj: Callable[..., object] | ast.FunctionDef,
+) -> None:
     if isinstance(handle_obj, ast.FunctionDef):
         name = handle_obj.name
         accepts_args = {arg.arg for arg in handle_obj.args.args}
@@ -913,7 +939,10 @@ def list(
         identifier = _get_function_identifier(function_id, function_external_id)
         function_id = _get_function_internal_id(self._cognite_client, identifier)
         filter = FunctionCallsFilter(
-            status=status, schedule_id=schedule_id, start_time=start_time, end_time=end_time
+            status=status,
+            schedule_id=schedule_id,
+            start_time=start_time,
+            end_time=end_time,
         ).dump(camel_case=True)
         resource_path = self._RESOURCE_PATH.format(function_id)
         return self._list(
@@ -926,7 +955,10 @@ def list(
         )
 
     def retrieve(
-        self, call_id: int, function_id: int | None = None, function_external_id: str | None = None
+        self,
+        call_id: int,
+        function_id: int | None = None,
+        function_external_id: str | None = None,
     ) -> FunctionCall | None:
         """`Retrieve a single function call by id. <https://developer.cognite.com/api#tag/Function-calls/operation/byIdsFunctionCalls>`_
 
@@ -964,7 +996,10 @@ def retrieve(
         )
 
     def get_response(
-        self, call_id: int, function_id: int | None = None, function_external_id: str | None = None
+        self,
+        call_id: int,
+        function_id: int | None = None,
+        function_external_id: str | None = None,
     ) -> dict[str, object] | None:
         """`Retrieve the response from a function call. <https://developer.cognite.com/api#tag/Function-calls/operation/getFunctionCallResponse>`_
 
@@ -997,7 +1032,10 @@ def get_response(
         return self._get(resource_path).json().get("response")
 
     def get_logs(
-        self, call_id: int, function_id: int | None = None, function_external_id: str | None = None
+        self,
+        call_id: int,
+        function_id: int | None = None,
+        function_external_id: str | None = None,
     ) -> FunctionCallLog:
         """`Retrieve logs for function call. <https://developer.cognite.com/api#tag/Function-calls/operation/getFunctionCalls>`_
 
@@ -1187,7 +1225,10 @@ def list(
             created_time=created_time,
             cron_expression=cron_expression,
         ).dump(camel_case=True)
-        res = self._post(url_path=f"{self._RESOURCE_PATH}/list", json={"filter": filter, "limit": limit})
+        res = self._post(
+            url_path=f"{self._RESOURCE_PATH}/list",
+            json={"filter": filter, "limit": limit},
+        )
 
         return FunctionSchedulesList._load(res.json()["items"], cognite_client=self._cognite_client)
 
@@ -1256,7 +1297,14 @@ def create(
         if isinstance(name, str):
             if cron_expression is None:
                 raise ValueError("cron_expression must be specified when creating a new schedule.")
-            item = FunctionScheduleWrite(name, cron_expression, function_id, function_external_id, description, data)
+            item = FunctionScheduleWrite(
+                name,
+                cron_expression,
+                function_id,
+                function_external_id,
+                description,
+                data,
+            )
         else:
             # We serialize the object as we mutate `item` using the result from _get_function_internal_id.
             item = FunctionScheduleWrite._load(name.dump())
@@ -1268,7 +1316,9 @@ def create(
 
         dumped = item.dump()
         dumped["nonce"] = create_session_and_return_nonce(
-            self._cognite_client, api_name="Functions API", client_credentials=client_credentials
+            self._cognite_client,
+            api_name="Functions API",
+            client_credentials=client_credentials,
         )
         return self._create_multiple(
             items=dumped,
diff --git a/cognite/client/data_classes/functions.py b/cognite/client/data_classes/functions.py
index 93b54adaf9..5cc5ed7a3c 100644
--- a/cognite/client/data_classes/functions.py
+++ b/cognite/client/data_classes/functions.py
@@ -2,7 +2,7 @@
 
 import time
 from abc import ABC
-from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast
+from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeAlias, cast
 
 from cognite.client._constants import DEFAULT_LIMIT_READ
 from cognite.client.data_classes._base import (
@@ -27,6 +27,54 @@
 HANDLER_FILE_NAME = "handler.py"
 
 
+class FunctionHandle(Protocol):
+    """The function handle.
+
+    This is the function that will be called when the function is executed. The function
+    must be named "handle" and can take any of the following named only arguments:
+
+    Args:
+        client (CogniteClient | None): Cognite client.
+        data (dict[str, object] | None): Input data to the function.
+        secrets (dict[str, str] | None): Secrets passed to the function.
+        function_call_info (dict[str, object] | None): Function call information.
+
+    Example:
+        .. code-block:: python
+
+            def handle(
+                client: CogniteClient | None = None,
+                data: dict[str, object] | None = None,
+            ) -> object:
+                # Do something with the data
+                return {"result": "success"}
+
+    Returns:
+        object: Return value of the function. Any JSON serializable object is allowed.
+    """
+
+    def __call__(
+        self,
+        *,
+        client: CogniteClient | None = None,
+        data: dict[str, object] | None = None,
+        secrets: dict[str, str] | None = None,
+        function_call_info: dict[str, object] | None = None,
+    ) -> object:
+        """Function handle protocol.
+
+        Args:
+            client (CogniteClient | None): Cognite client.
+            data (dict[str, object] | None): Input data to the function.
+            secrets (dict[str, str] | None): Secrets passed to the function.
+            function_call_info (dict[str, object] | None): Function call information.
+
+        Returns:
+            object: Return value of the function. Any JSON serializable object is allowed.
+        """
+        ...
+
+
 class FunctionCore(WriteableCogniteResource["FunctionWrite"], ABC):
     """A representation of a Cognite Function.
 
@@ -170,11 +218,11 @@ def as_write(self) -> FunctionWrite:
             metadata=self.metadata,
         )
 
-    def call(self, data: dict | None = None, wait: bool = True) -> FunctionCall:
+    def call(self, data: dict[str, object] | None = None, wait: bool = True) -> FunctionCall:
         """`Call this particular function. <https://docs.cognite.com/api/v1/#operation/postFunctionsCall>`_
 
         Args:
-            data (dict | None): Input data to the function (JSON serializable). This data is passed deserialized into the function through one of the arguments called data. **WARNING:** Secrets or other confidential information should not be passed via this argument. There is a dedicated `secrets` argument in FunctionsAPI.create() for this purpose.
+            data (dict[str, object] | None): Input data to the function (JSON serializable). This data is passed deserialized into the function through one of the arguments called data. **WARNING:** Secrets or other confidential information should not be passed via this argument. There is a dedicated `secrets` argument in FunctionsAPI.create() for this purpose.
             wait (bool): Wait until the function call is finished. Defaults to True.
 
         Returns:
@@ -530,7 +578,8 @@ class FunctionScheduleWriteList(CogniteResourceList[FunctionScheduleWrite]):
 
 
 class FunctionSchedulesList(
-    WriteableCogniteResourceList[FunctionScheduleWrite, FunctionSchedule], InternalIdTransformerMixin
+    WriteableCogniteResourceList[FunctionScheduleWrite, FunctionSchedule],
+    InternalIdTransformerMixin,
 ):
     _RESOURCE = FunctionSchedule
 
@@ -593,11 +642,11 @@ def __init__(
         self.function_id: int = function_id  # type: ignore
         self._cognite_client = cast("CogniteClient", cognite_client)
 
-    def get_response(self) -> dict | None:
+    def get_response(self) -> dict[str, object] | None:
         """Retrieve the response from this function call.
 
         Returns:
-            dict | None: Response from the function call.
+            dict[str, object] | None: Response from the function call.
         """
         call_id, function_id = self._get_identifiers_or_raise(self.id, self.function_id)
         return self._cognite_client.functions.calls.get_response(call_id=call_id, function_id=function_id)
diff --git a/scripts/custom_checks/docstrings.py b/scripts/custom_checks/docstrings.py
index f414b58d14..ce5f1c5d8b 100644
--- a/scripts/custom_checks/docstrings.py
+++ b/scripts/custom_checks/docstrings.py
@@ -22,11 +22,13 @@
 import numpy as np
 
 from cognite.client.data_classes.data_modeling.query import Query
+from cognite.client.data_classes.functions import FunctionHandle
 from cognite.client.utils._text import shorten
 
 FUNC_EXCEPTIONS = {}
 CLS_METHOD_EXCEPTIONS = {
     (Query, "__init__"),  # Reason: Uses a parameter 'with_'; and we need to escape the underscore
+    (FunctionHandle, "__init__"),  # Reason: Protocol class doesn't have __init__ with return type
 }
 
 # Helper for testing specific class + method/property:


Reviews:
user: login: qTipTip
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T12:27:09Z
user: login: qTipTip
body: Great! 

There are still a few references to `function_handle: Callable` in `_api.functions.py`  Those should also be typed as FunctionHandle. 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T12:31:14Z
user: login: dbrattli
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T12:42:37Z
user: login: dbrattli
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T12:42:51Z
user: login: qTipTip
body: LGTM! 
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T13:53:44Z
user: login: qTipTip
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T12:44:22Z
user: login: andersfylling
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T18:36:02Z
user: login: dbrattli
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T06:02:12Z

Review comments:
diff_hunk: @@ -17,6 +17,10 @@ Changes are grouped as follows
 - `Fixed` for any bug fixes.
 - `Security` in case of vulnerabilities.
 
+## [Unreleased]
+### Fixed
+- Fixes type annotations for Functions API
path: CHANGELOG.md
user: login: qTipTip
body: Maybe mention that FunctionHandle is now a new type. 
created_at: 2025-05-21T12:27:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -642,9 +660,15 @@ def _zip_and_upload_handle(
 
     @staticmethod
     def _assert_exactly_one_of_folder_or_file_id_or_function_handle(
-        folder: str | None, file_id: int | None, function_handle: Callable[..., Any] | None
+        folder: str | None,
+        file_id: int | None,
+        function_handle: Callable[..., Any] | None,
path: cognite/client/_api/functions.py
user: login: qTipTip
body: Change this to `FunctionHandle` as well? 
created_at: 2025-05-21T12:28:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -642,9 +660,15 @@ def _zip_and_upload_handle(
 
     @staticmethod
     def _assert_exactly_one_of_folder_or_file_id_or_function_handle(
-        folder: str | None, file_id: int | None, function_handle: Callable[..., Any] | None
+        folder: str | None,
+        file_id: int | None,
+        function_handle: Callable[..., Any] | None,
path: cognite/client/_api/functions.py
user: login: dbrattli
body: Fixed
created_at: 2025-05-21T12:42:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,6 +17,10 @@ Changes are grouped as follows
 - `Fixed` for any bug fixes.
 - `Security` in case of vulnerabilities.
 
+## [Unreleased]
+### Fixed
+- Fixes type annotations for Functions API
path: CHANGELOG.md
user: login: dbrattli
body: Added
created_at: 2025-05-21T12:42:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -766,7 +790,9 @@ def validate_function_folder(root_path: str, function_path: str, skip_folder_val
             _run_import_check_backup(root_path, module_path)
 
 
-def _validate_function_handle(handle_obj: Callable | ast.FunctionDef) -> None:
+def _validate_function_handle(
+    handle_obj: Callable[..., object] | ast.FunctionDef,
path: cognite/client/_api/functions.py
user: login: andersfylling
body: should this be `FunctionHandle | ast.FunctionDef`?
created_at: 2025-05-22T18:33:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -766,7 +790,9 @@ def validate_function_folder(root_path: str, function_path: str, skip_folder_val
             _run_import_check_backup(root_path, module_path)
 
 
-def _validate_function_handle(handle_obj: Callable | ast.FunctionDef) -> None:
+def _validate_function_handle(
+    handle_obj: Callable[..., object] | ast.FunctionDef,
path: cognite/client/_api/functions.py
user: login: dbrattli
body: No, not here. Verified with @qTipTip 
created_at: 2025-05-23T06:02:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-21T12:16:13Z
body: ## [Codecov](https://app.codecov.io/gh/cognitedata/cognite-sdk-python/pull/2157?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) Report
Attention: Patch coverage is `93.33333%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 90.63%. Comparing base [(`1a3be35`)](https://app.codecov.io/gh/cognitedata/cognite-sdk-python/commit/1a3be35cd9efa6a0fdf47b73f215fb0c64e1fc06?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) to head [(`eec2c65`)](https://app.codecov.io/gh/cognitedata/cognite-sdk-python/commit/eec2c65cbafca796cc8848e95d0461fad209198d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata).
> Report is 1 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/cognitedata/cognite-sdk-python/pull/2157?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) | Patch % | Lines |
|---|---|---|
| [cognite/client/data\_classes/functions.py](https://app.codecov.io/gh/cognitedata/cognite-sdk-python/pull/2157?src=pr&el=tree&filepath=cognite%2Fclient%2Fdata_classes%2Ffunctions.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Y29nbml0ZS9jbGllbnQvZGF0YV9jbGFzc2VzL2Z1bmN0aW9ucy5weQ==) | 83.33% | [1 Missing :warning: ](https://app.codecov.io/gh/cognitedata/cognite-sdk-python/pull/2157?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #2157      +/-   ##
==========================================
- Coverage   90.67%   90.63%   -0.05%     
==========================================
  Files         159      159              
  Lines       24036    24039       +3     
==========================================
- Hits        21794    21787       -7     
- Misses       2242     2252      +10     
```

| [Files with missing lines](https://app.codecov.io/gh/cognitedata/cognite-sdk-python/pull/2157?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) | Coverage Δ | |
|---|---|---|
| [cognite/client/\_api/functions.py](https://app.codecov.io/gh/cognitedata/cognite-sdk-python/pull/2157?src=pr&el=tree&filepath=cognite%2Fclient%2F_api%2Ffunctions.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Y29nbml0ZS9jbGllbnQvX2FwaS9mdW5jdGlvbnMucHk=) | `94.39% <100.00%> (ø)` | |
| [cognite/client/data\_classes/functions.py](https://app.codecov.io/gh/cognitedata/cognite-sdk-python/pull/2157?src=pr&el=tree&filepath=cognite%2Fclient%2Fdata_classes%2Ffunctions.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Y29nbml0ZS9jbGllbnQvZGF0YV9jbGFzc2VzL2Z1bmN0aW9ucy5weQ==) | `96.00% <83.33%> (-0.45%)` | :arrow_down: |

... and [3 files with indirect coverage changes](https://app.codecov.io/gh/cognitedata/cognite-sdk-python/pull/2157/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata)

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: haakonvt
created_at: 2025-05-21T17:05:20Z
body: Nice one! Please ensure the added protocol class is linked and visible in the documentation to help our users understand what it is. 

Afaik, the parameters don’t have to be keyword-only. 

Lastly, there’s a lot of unrelated formatting changes. Pre-commit or accidental?
author_association: CONTRIBUTOR
user: login: dbrattli
created_at: 2025-05-22T08:19:42Z
body: @haakonvt Added better documentation for the protocol class. Had to add an exception since the custom docstring parser choked on anything other than a one-liner for class documentation. Re-checked with the functions team that all the arguments are indeed named-only. Formatting changes are because of running `pre-commit`.
author_association: CONTRIBUTOR
user: login: andersfylling
created_at: 2025-05-22T18:37:00Z
body: :unicorn: 
author_association: CONTRIBUTOR