Skip to content

Commit

Permalink
Standardize function schedule create (#1858)
Browse files Browse the repository at this point in the history
  • Loading branch information
doctrino authored Jul 22, 2024
1 parent 8d5f541 commit 5658824
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 34 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ Changes are grouped as follows
- `Fixed` for any bug fixes.
- `Security` in case of vulnerabilities.

## [7.54.8] - 2024-07-22
### Added
- The method `client.functions.schedules.retrieve` now accepts the missing parameter `ignore_unknown_ids` as well
as retrieving multiple schedules at once.
- The method `client.functions.schedules.create` now supports creating using a `FunctionScheduleWrite` object.

### Changed
- When creating a new function schedule without specifying `description`, the default value is now
correctly set to `None` instead of `""`.

## [7.54.7] - 2024-07-22
### Fixed
- The method `client.three_d.models.update` no longer accepts `ThreeDModelWrite` as this will raise a `ValueError`.
Expand Down
73 changes: 44 additions & 29 deletions cognite/client/_api/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from cognite.client.data_classes.functions import (
HANDLER_FILE_NAME,
FunctionCallsFilter,
FunctionScheduleWrite,
FunctionsStatus,
FunctionStatus,
FunctionWrite,
Expand Down Expand Up @@ -1048,9 +1049,6 @@ def get_logs(
class FunctionSchedulesAPI(APIClient):
_RESOURCE_PATH = "/functions/schedules"

def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None:
super().__init__(config, api_version, cognite_client)

@overload
def __call__(
self,
Expand Down Expand Up @@ -1124,14 +1122,23 @@ def __iter__(self) -> Iterator[FunctionSchedule]:
"""Iterate over all function schedules"""
return self()

def retrieve(self, id: int) -> FunctionSchedule | None:
@overload
def retrieve(self, id: int, ignore_unknown_ids: bool = False) -> FunctionSchedule | None: ...

@overload
def retrieve(self, id: Sequence[int], ignore_unknown_ids: bool = False) -> FunctionSchedulesList: ...

def retrieve(
self, id: int | Sequence[int], ignore_unknown_ids: bool = False
) -> FunctionSchedule | None | FunctionSchedulesList:
"""`Retrieve a single function schedule by id. <https://developer.cognite.com/api#tag/Function-schedules/operation/byIdsFunctionSchedules>`_
Args:
id (int): Schedule ID
id (int | Sequence[int]): Schedule ID
ignore_unknown_ids (bool): Ignore IDs that are not found rather than throw an exception.
Returns:
FunctionSchedule | None: Requested function schedule or None if not found.
FunctionSchedule | None | FunctionSchedulesList: Requested function schedule or None if not found.
Examples:
Expand All @@ -1142,9 +1149,12 @@ def retrieve(self, id: int) -> FunctionSchedule | None:
>>> res = client.functions.schedules.retrieve(id=1)
"""
identifier = IdentifierSequence.load(ids=id).as_singleton()
identifiers = IdentifierSequence.load(ids=id)
return self._retrieve_multiple(
identifiers=identifier, resource_cls=FunctionSchedule, list_cls=FunctionSchedulesList
identifiers=identifiers,
resource_cls=FunctionSchedule,
list_cls=FunctionSchedulesList,
ignore_unknown_ids=ignore_unknown_ids,
)

def list(
Expand Down Expand Up @@ -1203,23 +1213,23 @@ def list(

def create(
self,
name: str,
cron_expression: str,
name: str | FunctionScheduleWrite,
cron_expression: str | None = None,
function_id: int | None = None,
function_external_id: str | None = None,
client_credentials: dict | ClientCredentials | None = None,
description: str = "",
description: str | None = None,
data: dict | None = None,
) -> FunctionSchedule:
"""`Create a schedule associated with a specific project. <https://developer.cognite.com/api#tag/Function-schedules/operation/postFunctionSchedules>`_
Args:
name (str): Name of the schedule.
cron_expression (str): Cron expression.
name (str | FunctionScheduleWrite): Name of the schedule or FunctionSchedule object. If a function schedule object is passed, the other arguments are ignored except for the client_credentials argument.
cron_expression (str | None): Cron expression.
function_id (int | None): Id of the function to attach the schedule to.
function_external_id (str | None): External id of the function to attach the schedule to. Will be converted to (internal) ID before creating the schedule.
client_credentials (dict | ClientCredentials | None): Instance of ClientCredentials or a dictionary containing client credentials: 'client_id' and 'client_secret'.
description (str): Description of the schedule.
description (str | None): Description of the schedule.
data (dict | None): Data to be passed to the scheduled run.
Returns:
Expand Down Expand Up @@ -1263,23 +1273,28 @@ def create(
... )
"""
identifier = _get_function_identifier(function_id, function_external_id)
function_id = _get_function_internal_id(self._cognite_client, identifier)
nonce = create_session_and_return_nonce(
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)
else:
item = name
identifier = _get_function_identifier(item.function_id, item.function_external_id)
if item.function_id is None:
item.function_id = _get_function_internal_id(self._cognite_client, identifier)
# API requires 'Exactly one of 'function_id' and 'function_external_id' must be set '
item.function_external_id = None

dumped = item.dump()
dumped["nonce"] = create_session_and_return_nonce(
self._cognite_client, api_name="Functions API", client_credentials=client_credentials
)
item = {
"name": name,
"description": description,
"functionId": function_id,
"cronExpression": cron_expression,
"nonce": nonce,
}
if data:
item["data"] = data

res = self._post(self._RESOURCE_PATH, json={"items": [item]})
return FunctionSchedule._load(res.json()["items"][0], cognite_client=self._cognite_client)
return self._create_multiple(
items=dumped,
resource_cls=FunctionSchedule,
input_resource_cls=FunctionScheduleWrite,
list_cls=FunctionSchedulesList,
)

def delete(self, id: int) -> None:
"""`Delete a schedule associated with a specific project. <https://developer.cognite.com/api#tag/Function-schedules/operation/deleteFunctionSchedules>`_
Expand Down
2 changes: 1 addition & 1 deletion cognite/client/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from __future__ import annotations

__version__ = "7.54.7"
__version__ = "7.54.8"
__api_subversion__ = "20230101"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
name = "cognite-sdk"

version = "7.54.7"
version = "7.54.8"
description = "Cognite Python SDK"
readme = "README.md"
documentation = "https://cognite-sdk-python.readthedocs-hosted.com"
Expand Down
83 changes: 81 additions & 2 deletions tests/tests_integration/test_api/test_functions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
from __future__ import annotations

from contextlib import suppress

import pytest

from cognite.client import CogniteClient
from cognite.client.data_classes import Function, FunctionList
from cognite.client.exceptions import CogniteNotFoundError
from cognite.client.data_classes import Function, FunctionList, FunctionSchedule, FunctionScheduleWrite
from cognite.client.exceptions import CogniteAPIError, CogniteNotFoundError


def handle(client, data) -> str:
return "Hello, world!"


@pytest.fixture(scope="module")
def a_function(cognite_client: CogniteClient) -> Function:
name = "python-sdk-test-function"
external_id = "python-sdk-test-function"
description = "test function"
retrieved = cognite_client.functions.retrieve_multiple(external_ids=[external_id], ignore_unknown_ids=True)
if retrieved:
return retrieved[0]

function = cognite_client.functions.create(
name=name,
external_id=external_id,
description=description,
function_handle=handle,
)
return function


class TestFunctionsAPI:
Expand Down Expand Up @@ -51,3 +75,58 @@ def test_iterate_chunked_functions(self, cognite_client: CogniteClient) -> None:
break
else:
assert False, "Expected at least one function"


class TestFunctionSchedulesAPI:
def test_create_retrieve_delete(self, cognite_client: CogniteClient, a_function: Function) -> None:
my_schedule = FunctionScheduleWrite(
name="python-sdk-test-schedule",
cron_expression="0 0 1 1 *",
function_external_id=a_function.external_id,
data={"key": "value"},
)

created: FunctionSchedule | None = None
try:
created = cognite_client.functions.schedules.create(my_schedule)

assert created.as_write().dump() == my_schedule.dump()

retrieved = cognite_client.functions.schedules.retrieve(id=created.id)
assert isinstance(retrieved, FunctionSchedule)
assert retrieved.dump() == created.dump()

cognite_client.functions.schedules.delete(id=created.id)
finally:
if created:
with suppress(CogniteAPIError):
cognite_client.functions.schedules.delete(id=created.id)

def test_retrieve_unknown(self, cognite_client: CogniteClient) -> None:
# The ID 123 should not exist
retrieved = cognite_client.functions.schedules.retrieve(id=123)

assert retrieved is None

def test_retrieve_known_and_unknown(self, cognite_client: CogniteClient, a_function: Function) -> None:
my_schedule = FunctionScheduleWrite(
name="python-sdk-test-schedule-known",
cron_expression="0 0 1 1 *",
function_id=a_function.id,
data={"key": "value"},
)
created: FunctionSchedule | None = None
try:
created = cognite_client.functions.schedules.create(my_schedule)

retrieved = cognite_client.functions.schedules.retrieve([created.id, 123], ignore_unknown_ids=True)
assert len(retrieved) == 1
assert retrieved[0].id == created.id
finally:
if created:
with suppress(CogniteAPIError):
cognite_client.functions.schedules.delete(id=created.id)

def test_raise_retrieve_unknown(self, cognite_client: CogniteClient) -> None:
with pytest.raises(CogniteNotFoundError):
cognite_client.functions.schedules.retrieve(id=[123])
2 changes: 1 addition & 1 deletion tests/tests_unit/test_api/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,7 @@ def test_create_schedules_with_function_external_id(self, cognite_client):
description="Hi",
)

call_args = post_mock.call_args[1]["json"]["items"][0]
call_args = post_mock.call_args[0][1]["items"][0]
assert "functionId" in call_args
assert "functionExternalId" not in call_args
assert isinstance(res, FunctionSchedule)
Expand Down

0 comments on commit 5658824

Please sign in to comment.