Skip to content

Commit 30adcf7

Browse files
committed
feat; Implemented ProjectUpdate
1 parent f76ce95 commit 30adcf7

File tree

5 files changed

+258
-25
lines changed

5 files changed

+258
-25
lines changed

cognite/client/_api/projects.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from typing import Sequence, overload
4+
from urllib.parse import quote
45

56
from cognite.client._api_client import APIClient
67
from cognite.client.data_classes import Project, ProjectList, ProjectUpdate, ProjectURLNameList, ProjectWrite
@@ -23,12 +24,16 @@ def create(self, item: ProjectWrite | Sequence[ProjectWrite]) -> Project | Proje
2324

2425
def retrieve(self, project: str) -> Project:
2526
"""`Retrieve a project <https://developer.cognite.com/api#tag/Projects/operation/getProject>`_"""
26-
item = self._get(f"{self._RESOURCE_PATH}/{project}")
27+
item = self._get(f"{self._RESOURCE_PATH}/{quote(project, '')}")
2728
return Project._load(item.json(), cognite_client=self._cognite_client)
2829

29-
def update(self, item: ProjectWrite | ProjectUpdate) -> Project:
30+
def update(self, item: ProjectUpdate) -> Project:
3031
"""`Update a project <https://developer.cognite.com/api#tag/Projects/operation/updateProject>`_"""
31-
return self._update_multiple(item, list_cls=ProjectList, resource_cls=Project, update_cls=ProjectUpdate)
32+
project = item._project
33+
response = self._post(
34+
url_path=f"{self._RESOURCE_PATH}/{quote(project, '')}/update", json=item.dump(camel_case=True)
35+
)
36+
return Project._load(response.json(), cognite_client=self._cognite_client)
3237

3338
def list(self) -> ProjectURLNameList:
3439
"""`List all projects <https://developer.cognite.com/api#tag/Projects/operation/listProjects>`_"""

cognite/client/data_classes/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@
287287
TransformationSchemaColumn,
288288
TransformationSchemaColumnList,
289289
)
290-
from cognite.client.data_classes.user_profiles import UserProfile, UserProfileList
290+
from cognite.client.data_classes.user_profiles import UserProfile, UserProfileList, UserProfilesConfiguration
291291
from cognite.client.data_classes.workflows import (
292292
CancelExecution,
293293
CDFTaskOutput,
@@ -542,6 +542,7 @@
542542
"CoordinateReferenceSystem",
543543
"UserProfile",
544544
"UserProfileList",
545+
"UserProfilesConfiguration",
545546
"CancelExecution",
546547
"WorkflowUpsert",
547548
"WorkflowExecution",

cognite/client/data_classes/projects.py

Lines changed: 174 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
from __future__ import annotations
22

33
from abc import ABC
4-
from typing import TYPE_CHECKING, Any
4+
from typing import TYPE_CHECKING, Any, Generic, Literal, Sequence
55

66
from cognite.client.data_classes._base import (
77
CogniteObject,
8-
CogniteObjectUpdate,
98
CognitePrimitiveUpdate,
109
CogniteResource,
1110
CogniteResourceList,
1211
CogniteUpdate,
1312
PropertySpec,
13+
T_CogniteUpdate,
1414
WriteableCogniteResource,
1515
)
1616
from cognite.client.data_classes.user_profiles import UserProfilesConfiguration
@@ -283,48 +283,202 @@ def dump(self, camel_case: bool = True) -> dict[str, Any]:
283283
return output
284284

285285

286+
# Move into _base?
287+
class _CogniteNestedUpdate(Generic[T_CogniteUpdate]):
288+
def __init__(self, parent_object: T_CogniteUpdate, name: str) -> None:
289+
self._parent_object = parent_object
290+
self._name = name
291+
292+
def _set(self, value: CogniteObject | None) -> T_CogniteUpdate:
293+
if self._name not in self._parent_object._update_object:
294+
self._parent_object._update_object[self._name] = {}
295+
update_object = self._parent_object._update_object[self._name]
296+
if "modify" in update_object:
297+
raise RuntimeError("Cannot set and modify the same property")
298+
if value is None:
299+
update_object["setNull"] = True
300+
else:
301+
update_object["set"] = value.dump(camel_case=True)
302+
return self._parent_object
303+
304+
305+
class _CogniteNestedUpdateProperty(Generic[T_CogniteUpdate]):
306+
def __init__(self, parent_object: T_CogniteUpdate, parent_name: str, name: str) -> None:
307+
self._parent_object = parent_object
308+
self._parent_name = parent_name
309+
self._name = name
310+
311+
@property
312+
def _update_object(self) -> dict[str, Any]:
313+
if self._parent_name not in self._parent_object._update_object:
314+
self._parent_object._update_object[self._parent_name] = {}
315+
update_object = self._parent_object._update_object[self._parent_name]
316+
if "set" in update_object:
317+
raise RuntimeError("Cannot set and modify the same property")
318+
if "modify" not in update_object:
319+
update_object["modify"] = {}
320+
if self._name in update_object["modify"]:
321+
raise RuntimeError(f"Cannot modify {self._name} twice")
322+
return update_object
323+
324+
325+
class _CogniteNestedPrimitiveUpdate(_CogniteNestedUpdateProperty[T_CogniteUpdate]):
326+
def _set(self, value: None | str | int | bool) -> T_CogniteUpdate:
327+
update_object = self._update_object
328+
if self._parent_name == "userProfilesConfiguration" and self._name == "enabled":
329+
# Bug in Spec?
330+
update_object["modify"][self._name] = value
331+
elif value is None:
332+
update_object["modify"][self._name] = {"setNull": True}
333+
else:
334+
update_object["modify"][self._name] = {"set": value}
335+
return self._parent_object
336+
337+
338+
class _CogniteNestedListUpdate(_CogniteNestedUpdateProperty[T_CogniteUpdate]):
339+
def _update_modify_object(
340+
self, values: CogniteObject | Sequence[CogniteObject], word: Literal["set", "add", "remove"]
341+
) -> T_CogniteUpdate:
342+
update_object = self._update_object
343+
value_list = [values] if isinstance(values, CogniteObject) else values
344+
if update_object["modify"].get(self._name) is not None:
345+
raise RuntimeError(f"Cannot {word} and modify the same property twice")
346+
update_object["modify"][self._name] = {word: [value.dump(camel_case=True) for value in value_list]}
347+
return self._parent_object
348+
349+
def _set(self, values: CogniteObject | Sequence[CogniteObject]) -> T_CogniteUpdate:
350+
return self._update_modify_object(values, "set")
351+
352+
def _add(self, values: CogniteObject | Sequence[CogniteObject]) -> T_CogniteUpdate:
353+
return self._update_modify_object(values, "add")
354+
355+
def _remove(self, values: CogniteObject | Sequence[CogniteObject]) -> T_CogniteUpdate:
356+
return self._update_modify_object(values, "remove")
357+
358+
286359
class ProjectUpdate(CogniteUpdate):
287360
"""Changes applied to a Project.
288361
289362
Args:
290-
project: The Project to be updated.
363+
project (str): The Project to be updated.
291364
"""
292365

293-
class _PrimitiveProjectUpdate(CognitePrimitiveUpdate):
366+
def __init__(self, project: str) -> None:
367+
super().__init__(None, None)
368+
self._project = project
369+
370+
class _PrimitiveProjectUpdate(CognitePrimitiveUpdate["ProjectUpdate"]):
294371
def set(self, value: str) -> ProjectUpdate:
295372
return self._set(value)
296373

297-
class _OIDCProjectUpdate(CogniteObjectUpdate):
374+
class _NestedPrimitiveUpdateNullable(_CogniteNestedPrimitiveUpdate["ProjectUpdate"]):
375+
def set(self, value: str | bool | int | None) -> ProjectUpdate:
376+
return self._set(value)
377+
378+
class _NestedPrimitiveUpdate(_CogniteNestedPrimitiveUpdate["ProjectUpdate"]):
379+
def set(self, value: str | bool | int) -> ProjectUpdate:
380+
return self._set(value)
381+
382+
class _NestedListUpdate(_CogniteNestedListUpdate["ProjectUpdate"]):
383+
def set(self, values: Claim | Sequence[Claim]) -> ProjectUpdate:
384+
return self._set(values)
385+
386+
def add(self, values: Claim | Sequence[Claim]) -> ProjectUpdate:
387+
return self._add(values)
388+
389+
def remove(self, values: Claim | Sequence[Claim]) -> ProjectUpdate:
390+
return self._remove(values)
391+
392+
class _NestedOIDCConfiguration(_CogniteNestedUpdate["ProjectUpdate"]):
393+
class _OIDCConfigurationUpdate:
394+
def __init__(self, parent_object: ProjectUpdate, name: str) -> None:
395+
self._parent_object = parent_object
396+
self._name = name
397+
398+
@property
399+
def jwks_url(self) -> ProjectUpdate._NestedPrimitiveUpdate:
400+
return ProjectUpdate._NestedPrimitiveUpdate(self._parent_object, self._name, "jwksUrl")
401+
402+
@property
403+
def token_url(self) -> ProjectUpdate._NestedPrimitiveUpdateNullable:
404+
return ProjectUpdate._NestedPrimitiveUpdateNullable(self._parent_object, self._name, "tokenUrl")
405+
406+
@property
407+
def issuer(self) -> ProjectUpdate._NestedPrimitiveUpdate:
408+
return ProjectUpdate._NestedPrimitiveUpdate(self._parent_object, self._name, "issuer")
409+
410+
@property
411+
def audience(self) -> ProjectUpdate._NestedPrimitiveUpdate:
412+
return ProjectUpdate._NestedPrimitiveUpdate(self._parent_object, self._name, "audience")
413+
414+
@property
415+
def skew_ms(self) -> ProjectUpdate._NestedPrimitiveUpdateNullable:
416+
return ProjectUpdate._NestedPrimitiveUpdateNullable(self._parent_object, self._name, "skewMs")
417+
418+
@property
419+
def access_claims(self) -> ProjectUpdate._NestedListUpdate:
420+
return ProjectUpdate._NestedListUpdate(self._parent_object, self._name, "accessClaims")
421+
422+
@property
423+
def scope_claims(self) -> ProjectUpdate._NestedListUpdate:
424+
return ProjectUpdate._NestedListUpdate(self._parent_object, self._name, "scopeClaims")
425+
426+
@property
427+
def log_claims(self) -> ProjectUpdate._NestedListUpdate:
428+
return ProjectUpdate._NestedListUpdate(self._parent_object, self._name, "logClaims")
429+
430+
@property
431+
def is_group_callback_enabled(self) -> ProjectUpdate._NestedPrimitiveUpdateNullable:
432+
return ProjectUpdate._NestedPrimitiveUpdateNullable(
433+
self._parent_object, self._name, "isGroupCallbackEnabled"
434+
)
435+
436+
@property
437+
def identity_provider_scope(self) -> ProjectUpdate._NestedPrimitiveUpdateNullable:
438+
return ProjectUpdate._NestedPrimitiveUpdateNullable(
439+
self._parent_object, self._name, "identityProviderScope"
440+
)
441+
298442
def set(self, value: OIDCConfiguration | None) -> ProjectUpdate:
299-
return self._set((value and value.dump()) or {})
443+
return self._set(value)
300444

301-
def modify(self) -> ProjectUpdate:
302-
raise NotImplementedError
445+
@property
446+
def modify(self) -> _OIDCConfigurationUpdate:
447+
return self._OIDCConfigurationUpdate(self._parent_object, self._name)
448+
449+
class _NestedUserProfilesConfiguration(_CogniteNestedUpdate["ProjectUpdate"]):
450+
class _UserProfilesConfigurationUpdate:
451+
def __init__(self, parent_object: ProjectUpdate, name: str) -> None:
452+
self._parent_object = parent_object
453+
self._name = name
454+
455+
@property
456+
def enabled(self) -> ProjectUpdate._NestedPrimitiveUpdateNullable:
457+
return ProjectUpdate._NestedPrimitiveUpdateNullable(self._parent_object, self._name, "enabled")
303458

304-
class _UserProfileProjectUpdate(CogniteObjectUpdate):
305459
def set(self, value: UserProfilesConfiguration) -> ProjectUpdate:
306-
return self._set(value.dump())
460+
return self._set(value)
307461

308-
def modify(self) -> ProjectUpdate:
309-
raise NotImplementedError
462+
@property
463+
def modify(self) -> _UserProfilesConfigurationUpdate:
464+
return self._UserProfilesConfigurationUpdate(self._parent_object, self._name)
310465

311466
@property
312-
def project(self) -> ProjectUpdate._PrimitiveProjectUpdate:
313-
return ProjectUpdate._PrimitiveProjectUpdate(self, "project")
467+
def name(self) -> _PrimitiveProjectUpdate:
468+
return ProjectUpdate._PrimitiveProjectUpdate(self, "name")
314469

315470
@property
316-
def oidc_configuration(self) -> ProjectUpdate._OIDCProjectUpdate:
317-
return ProjectUpdate._OIDCProjectUpdate(self, "oidcConfiguration")
471+
def oidc_configuration(self) -> _NestedOIDCConfiguration:
472+
return ProjectUpdate._NestedOIDCConfiguration(self, "oidcConfiguration")
318473

319474
@property
320-
def user_profiles_configuration(self) -> ProjectUpdate._UserProfileProjectUpdate:
321-
return ProjectUpdate._UserProfileProjectUpdate(self, "userProfilesConfiguration")
475+
def user_profiles_configuration(self) -> _NestedUserProfilesConfiguration:
476+
return ProjectUpdate._NestedUserProfilesConfiguration(self, "userProfilesConfiguration")
322477

323478
@classmethod
324479
def _get_update_properties(cls) -> list[PropertySpec]:
325480
return [
326-
# External ID is nullable, but is used in the upsert logic and thus cannot be nulled out.
327-
PropertySpec("project", is_nullable=False),
481+
PropertySpec("name", is_nullable=False),
328482
PropertySpec("oidc_configuration", is_nullable=True),
329483
PropertySpec("user_profiles_configuration", is_nullable=False),
330484
]

tests/tests_unit/test_base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from cognite.client import ClientConfig, CogniteClient
1414
from cognite.client.credentials import Token
15-
from cognite.client.data_classes import Feature, FeatureAggregate
15+
from cognite.client.data_classes import Feature, FeatureAggregate, Project
1616
from cognite.client.data_classes._base import (
1717
CogniteFilter,
1818
CogniteLabelUpdate,
@@ -194,6 +194,8 @@ def test_dump_load_only_required(
194194
[
195195
pytest.param(class_, id=f"{class_.__name__} in {class_.__module__}")
196196
for class_ in all_concrete_subclasses(WriteableCogniteResource)
197+
# Project does not support as_write
198+
if class_ is not Project
197199
],
198200
)
199201
def test_writable_as_write(
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Iterable
4+
5+
import pytest
6+
from _pytest.mark import ParameterSet
7+
8+
from cognite.client.data_classes import Claim, ProjectUpdate, UserProfilesConfiguration
9+
10+
11+
def project_update_dump_test_cases() -> Iterable[ParameterSet]:
12+
update = ProjectUpdate("my_project").name.set("new_name")
13+
yield pytest.param(
14+
update,
15+
{"update": {"name": {"set": "new_name"}}},
16+
id="Set name",
17+
)
18+
update = ProjectUpdate("my_project").user_profiles_configuration.modify.enabled.set(False).name.set("new_name")
19+
20+
yield pytest.param(
21+
update,
22+
{"update": {"userProfilesConfiguration": {"modify": {"enabled": False}}, "name": {"set": "new_name"}}},
23+
id="Modify user profiles configuration",
24+
)
25+
update = ProjectUpdate("my_project").user_profiles_configuration.set(UserProfilesConfiguration(enabled=True))
26+
27+
yield pytest.param(
28+
update,
29+
{"update": {"userProfilesConfiguration": {"set": {"enabled": True}}}},
30+
id="Set user profiles configuration",
31+
)
32+
33+
update = ProjectUpdate("my_project").oidc_configuration.set(None).name.set("new_name")
34+
yield pytest.param(
35+
update,
36+
{"update": {"oidcConfiguration": {"setNull": True}, "name": {"set": "new_name"}}},
37+
id="Set oidc configuration and name",
38+
)
39+
40+
update = (
41+
ProjectUpdate("my_project")
42+
.oidc_configuration.modify.jwks_url.set("new_url")
43+
.oidc_configuration.modify.skew_ms.set(None)
44+
)
45+
46+
yield pytest.param(
47+
update,
48+
{"update": {"oidcConfiguration": {"modify": {"jwksUrl": {"set": "new_url"}, "skewMs": {"setNull": True}}}}},
49+
id="Modify oidc configuration",
50+
)
51+
52+
update = (
53+
ProjectUpdate("my_project").oidc_configuration.modify.access_claims.add(Claim("new_claim")).name.set("new_name")
54+
)
55+
56+
yield pytest.param(
57+
update,
58+
{
59+
"update": {
60+
"oidcConfiguration": {"modify": {"accessClaims": {"add": [{"claimName": "new_claim"}]}}},
61+
"name": {"set": "new_name"},
62+
}
63+
},
64+
id="Modify oidc configuration",
65+
)
66+
67+
68+
class TestProjectUpdate:
69+
@pytest.mark.parametrize("project_update, expected_dump", list(project_update_dump_test_cases()))
70+
def test_dump(self, project_update: ProjectUpdate, expected_dump: dict[str, Any]) -> None:
71+
assert project_update.dump() == expected_dump

0 commit comments

Comments
 (0)