diff --git a/CHANGELOG.md b/CHANGELOG.md index e221260a6..2600689b5 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. +## [7.62.8] - 2024-10-07 +### Added +- [Feature Preview - alpha] Support for `PostgresGateway` `Users` `client.postegres_gateway.users`. + ## [7.62.7] - 2024-10-07 ### Fixed - Several bugfixes for the filter `InAssetSubtree`: diff --git a/cognite/client/_api/postgres_gateway/__init__.py b/cognite/client/_api/postgres_gateway/__init__.py new file mode 100644 index 000000000..2b112780a --- /dev/null +++ b/cognite/client/_api/postgres_gateway/__init__.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from cognite.client._api.postgres_gateway.users import UsersAPI +from cognite.client._api_client import APIClient + +if TYPE_CHECKING: + from cognite.client import CogniteClient + from cognite.client.config import ClientConfig + + +class PostgresGatewaysAPI(APIClient): + def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None: + super().__init__(config, api_version, cognite_client) + self.users = UsersAPI(config, api_version, cognite_client) diff --git a/cognite/client/_api/postgres_gateway/users.py b/cognite/client/_api/postgres_gateway/users.py new file mode 100644 index 000000000..052fa991b --- /dev/null +++ b/cognite/client/_api/postgres_gateway/users.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +from collections.abc import Iterator +from typing import TYPE_CHECKING, Sequence, overload + +from cognite.client._api_client import APIClient +from cognite.client._constants import DEFAULT_LIMIT_READ +from cognite.client.data_classes.postgres_gateway.users import User, UserList, UserUpdate, UserWrite +from cognite.client.utils._experimental import FeaturePreviewWarning +from cognite.client.utils._identifier import UsernameSequence +from cognite.client.utils.useful_types import SequenceNotStr + +if TYPE_CHECKING: + from cognite.client import ClientConfig, CogniteClient + + +class UsersAPI(APIClient): + _RESOURCE_PATH = "/postgresgateway" + + def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None: + super().__init__(config, api_version, cognite_client) + self._warning = FeaturePreviewWarning(api_maturity="beta", sdk_maturity="alpha", feature_name="Users") + + @overload + def __call__( + self, + chunk_size: None = None, + limit: int | None = None, + ) -> Iterator[User]: ... + + @overload + def __call__( + self, + chunk_size: int, + limit: int | None = None, + ) -> Iterator[UserList]: ... + + def __call__( + self, + chunk_size: int | None = None, + limit: int | None = None, + ) -> Iterator[User] | Iterator[UserList]: + """Iterate over users + + Fetches user as they are iterated over, so you keep a limited number of users in memory. + + Args: + chunk_size (int | None): Number of users to return in each chunk. Defaults to yielding one user at a time. + limit (int | None): Maximum number of users to return. Defaults to return all. + + + Returns: + Iterator[User] | Iterator[UserList]: yields User one by one if chunk_size is not specified, else UserList objects. + """ + self._warning.warn() + + return self._list_generator( + list_cls=UserList, + resource_cls=User, + method="GET", + chunk_size=chunk_size, + limit=limit, + headers={"cdf-version": "beta"}, + ) + + def __iter__(self) -> Iterator[User]: + """Iterate over users + + Fetches users as they are iterated over, so you keep a + limited number of users in memory. + + Returns: + Iterator[User]: yields user one by one. + """ + return self() + + @overload + def create(self, user: UserWrite) -> User: ... + + @overload + def create(self, user: Sequence[UserWrite]) -> UserList: ... + + def create(self, user: UserWrite | Sequence[UserWrite]) -> User | UserList: + """`Create Users `_ + + Create postgres users. + + Args: + user (UserWrite | Sequence[UserWrite]): The user(s) to create. + + Returns: + User | UserList: The created user(s) + + Examples: + + Create user: + + >>> import os + >>> from cognite.client import CogniteClient + >>> from cognite.client.data_classes.postgres_gateway import UserWrite, SessionCredentials + >>> from cognite.client.data_classes import ClientCredentials + >>> client = CogniteClient() + >>> session = client.iam.sessions.create( + ... ClientCredentials(os.environ["IDP_CLIENT_ID"], os.environ["IDP_CLIENT_SECRET"]), + ... session_type="CLIENT_CREDENTIALS" + ... ) + >>> user = UserWrite(credentials=SessionCredentials(nonce=session.nonce)) + >>> res = client.postgres_gateway.users.create(user) + + """ + self._warning.warn() + return self._create_multiple( + list_cls=UserList, + resource_cls=User, + items=user, + input_resource_cls=UserWrite, + headers={"cdf-version": "beta"}, + ) + + @overload + def update(self, items: UserUpdate | UserWrite) -> User: ... + + @overload + def update(self, items: Sequence[UserUpdate | UserWrite]) -> UserList: ... + + def update(self, items: UserUpdate | UserWrite | Sequence[UserUpdate | UserWrite]) -> User | UserList: + """`Update users `_ + + Update postgres users + + Args: + items (UserUpdate | UserWrite | Sequence[UserUpdate | UserWrite]): The user(s) to update. + + Returns: + User | UserList: The updated user(s) + + Examples: + + Update user: + + >>> import os + >>> from cognite.client import CogniteClient + >>> from cognite.client.data_classes.postgres_gateway import UserUpdate, SessionCredentials + >>> from cognite.client.data_classes import ClientCredentials + >>> client = CogniteClient() + >>> session = client.iam.sessions.create( + ... ClientCredentials(os.environ["IDP_CLIENT_ID"], os.environ["IDP_CLIENT_SECRET"]), + ... session_type="CLIENT_CREDENTIALS" + ... ) + >>> update = UserUpdate('myUser').credentials.set(SessionCredentials(nonce=session.nonce)) + >>> res = client.postgres_gateway.users.update(update) + + """ + self._warning.warn() + return self._update_multiple( + items=items, + list_cls=UserList, + resource_cls=User, + update_cls=UserUpdate, + headers={"cdf-version": "beta"}, + ) + + def delete(self, username: str | SequenceNotStr[str], ignore_unknown_ids: bool = False) -> None: + """`Delete postgres user(s) `_ + + Delete postgres users + + Args: + username (str | SequenceNotStr[str]): Usernames of the users to delete. + ignore_unknown_ids (bool): Ignore usernames that are not found + + + Examples: + + Delete users: + + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> client.postgres_gateway.users.delete(["myUser", "myUser2"]) + + + """ + self._warning.warn() + extra_body_fields = {"ignore_unknown_ids": ignore_unknown_ids} + + self._delete_multiple( + identifiers=UsernameSequence.load(usernames=username), + wrap_ids=True, + returns_items=False, + extra_body_fields=extra_body_fields, + headers={"cdf-version": "beta"}, + ) + + @overload + def retrieve(self, username: str, ignore_unknown_ids: bool = False) -> User: ... + + @overload + def retrieve(self, username: SequenceNotStr[str], ignore_unknown_ids: bool = False) -> UserList: ... + + def retrieve(self, username: str | SequenceNotStr[str], ignore_unknown_ids: bool = False) -> User | UserList: + """`Retrieve a list of users by their usernames `_ + + Retrieve a list of postgres users by their usernames, optionally ignoring unknown usernames + + Args: + username (str | SequenceNotStr[str]): Usernames of the users to retrieve. + ignore_unknown_ids (bool): Ignore usernames that are not found + + Returns: + User | UserList: The retrieved user(s). + + Examples: + + Retrieve user: + + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> res = client.postgres_gateway.users.retrieve("myUser", ignore_unknown_ids=True) + + """ + self._warning.warn() + + return self._retrieve_multiple( + list_cls=UserList, + resource_cls=User, + identifiers=UsernameSequence.load(usernames=username), + ignore_unknown_ids=ignore_unknown_ids, + headers={"cdf-version": "beta"}, + ) + + def list(self, limit: int = DEFAULT_LIMIT_READ) -> UserList: + """`Fetch scoped users `_ + + List all users in a given project. + + Args: + limit (int): Limits the number of results to be returned. + + Returns: + UserList: A list of users + + Examples: + + List users: + + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> user_list = client.postgres_gateway.users.list(limit=5) + + Iterate over users:: + + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> for user in client.postgres_gateway.users: + ... user # do something with the user + + Iterate over chunks of users to reduce memory load:: + + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> for user_list in client.postgres_gateway.users(chunk_size=25): + ... user_list # do something with the users + + """ + self._warning.warn() + return self._list( + list_cls=UserList, + resource_cls=User, + method="GET", + limit=limit, + headers={"cdf-version": "beta"}, + ) diff --git a/cognite/client/_api_client.py b/cognite/client/_api_client.py index 961ec5705..c7c438e9f 100644 --- a/cognite/client/_api_client.py +++ b/cognite/client/_api_client.py @@ -108,6 +108,7 @@ class APIClient: "extpipes/(list|byids|runs/list)", "workflows/.*", "hostedextractors/.*", + "postgresgateway/.*", ) ) ] diff --git a/cognite/client/_cognite_client.py b/cognite/client/_cognite_client.py index 12d83162f..b5c0f01b4 100644 --- a/cognite/client/_cognite_client.py +++ b/cognite/client/_cognite_client.py @@ -19,6 +19,7 @@ from cognite.client._api.hosted_extractors import HostedExtractorsAPI from cognite.client._api.iam import IAMAPI from cognite.client._api.labels import LabelsAPI +from cognite.client._api.postgres_gateway import PostgresGatewaysAPI from cognite.client._api.raw import RawAPI from cognite.client._api.relationships import RelationshipsAPI from cognite.client._api.sequences import SequencesAPI @@ -73,6 +74,7 @@ def __init__(self, config: ClientConfig | None = None) -> None: self.vision = VisionAPI(self._config, self._API_VERSION, self) self.extraction_pipelines = ExtractionPipelinesAPI(self._config, self._API_VERSION, self) self.hosted_extractors = HostedExtractorsAPI(self._config, self._API_VERSION, self) + self.postgres_gateway = PostgresGatewaysAPI(self._config, self._API_VERSION, self) self.transformations = TransformationsAPI(self._config, self._API_VERSION, self) self.diagrams = DiagramsAPI(self._config, self._API_VERSION, self) self.annotations = AnnotationsAPI(self._config, self._API_VERSION, self) diff --git a/cognite/client/_version.py b/cognite/client/_version.py index 9833217b3..98a258d16 100644 --- a/cognite/client/_version.py +++ b/cognite/client/_version.py @@ -1,4 +1,4 @@ from __future__ import annotations -__version__ = "7.62.7" +__version__ = "7.62.8" __api_subversion__ = "20230101" diff --git a/cognite/client/data_classes/capabilities.py b/cognite/client/data_classes/capabilities.py index b5614442f..af250630c 100644 --- a/cognite/client/data_classes/capabilities.py +++ b/cognite/client/data_classes/capabilities.py @@ -1286,6 +1286,21 @@ class Scope: All = AllScope +@dataclass +class PostgresGatewayAcl(Capability): + _capability_name = "postgresGatewayAcl" + actions: Sequence[Action] + scope: AllScope = field(default_factory=AllScope) + allow_unknown: bool = field(default=False, compare=False, repr=False) + + class Action(Capability.Action): # type: ignore [misc] + Read = "READ" + Write = "WRITE" + + class Scope: + All = AllScope + + @dataclass class UserProfilesAcl(Capability): _capability_name = "userProfilesAcl" diff --git a/cognite/client/data_classes/postgres_gateway/__init__.py b/cognite/client/data_classes/postgres_gateway/__init__.py new file mode 100644 index 000000000..b5ec0c662 --- /dev/null +++ b/cognite/client/data_classes/postgres_gateway/__init__.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from cognite.client.data_classes.postgres_gateway.users import ( + SessionCredentials, + User, + UserList, + UserUpdate, + UserWrite, + UserWriteList, +) + +__all__ = ["User", "UserList", "UserUpdate", "UserWrite", "UserWriteList", "SessionCredentials"] diff --git a/cognite/client/data_classes/postgres_gateway/users.py b/cognite/client/data_classes/postgres_gateway/users.py new file mode 100644 index 000000000..06f48f862 --- /dev/null +++ b/cognite/client/data_classes/postgres_gateway/users.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +from abc import ABC +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Literal, NoReturn + +from typing_extensions import Self + +from cognite.client.data_classes._base import ( + CogniteObject, + CognitePrimitiveUpdate, + CogniteResource, + CogniteResourceList, + CogniteUpdate, + PropertySpec, + WriteableCogniteResource, + WriteableCogniteResourceList, +) + +if TYPE_CHECKING: + from cognite.client import CogniteClient + + +@dataclass +class SessionCredentials(CogniteObject): + nonce: str + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + nonce=resource["nonce"], + ) + + +class _UserCore(WriteableCogniteResource["UserWrite"], ABC): ... + + +class UserWrite(_UserCore): + """A postgres gateway **user** (also a typical postgres user) owns the foreign tables (built in or custom). + + The created postgres user only has access to use foreign tables and cannot directly create tables users. To create + foreign tables use the Postgres Gateway Tables APIs + + This is the write/request format of the user. + + Args: + credentials (SessionCredentials | None): Credentials for authenticating towards CDF using a CDF session. + + """ + + def __init__(self, credentials: SessionCredentials | None = None) -> None: + self.credentials = credentials + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + credentials=SessionCredentials._load(resource["credentials"], cognite_client) + if "credentials" in resource + else None, + ) + + def dump(self, camel_case: bool = True) -> dict[str, Any]: + output = super().dump(camel_case=camel_case) + if isinstance(self.credentials, SessionCredentials): + output["credentials"] = self.credentials.dump(camel_case=camel_case) + + return output + + def as_write(self) -> UserWrite: + return self + + +class User(_UserCore): + """A user. + + This is the read/response format of the user. + + Args: + username (str): Username to authenticate the user on the DB. + created_time (int): The number of milliseconds since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds. + last_updated_time (int): The number of milliseconds since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds. + session_id (int): ID of the session tied to this user. + + """ + + def __init__(self, username: str, created_time: int, last_updated_time: int, session_id: int) -> None: + self.username = username + self.created_time = created_time + self.last_updated_time = last_updated_time + self.session_id = session_id + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + username=resource["username"], + created_time=resource["createdTime"], + last_updated_time=resource["lastUpdatedTime"], + session_id=resource["sessionId"], + ) + + def as_write(self) -> NoReturn: + raise TypeError(f"{type(self).__name__} cannot be converted to a write object") + + +class UserUpdate(CogniteUpdate): + def __init__( + self, + username: str, + ) -> None: + super().__init__() + self.username = username + + class _UpdateItemSessionCredentialsUpdate(CognitePrimitiveUpdate): + def set(self, value: SessionCredentials | None) -> UserUpdate: + return self._set(value.dump() if isinstance(value, SessionCredentials) else value) + + @property + def credentials(self) -> UserUpdate._UpdateItemSessionCredentialsUpdate: + return self._UpdateItemSessionCredentialsUpdate(self, "credentials") + + @classmethod + def _get_update_properties(cls, item: CogniteResource | None = None) -> list[PropertySpec]: + return [ + PropertySpec("credentials", is_nullable=True), + ] + + def dump(self, camel_case: Literal[True] = True) -> dict[str, Any]: + """Dump the instance into a json serializable Python data type. + + Args: + camel_case (Literal[True]): No description. + Returns: + dict[str, Any]: A dictionary representation of the instance. + """ + return {"update": self._update_object, "username": self.username} + + +class UserWriteList(CogniteResourceList[UserWrite]): + _RESOURCE = UserWrite + + +class UserList(WriteableCogniteResourceList[UserWrite, User]): + _RESOURCE = User + + def as_write(self) -> NoReturn: + raise TypeError(f"{type(self).__name__} cannot be converted to a write object") diff --git a/cognite/client/testing.py b/cognite/client/testing.py index 0cb8b382c..ec209ed68 100644 --- a/cognite/client/testing.py +++ b/cognite/client/testing.py @@ -36,6 +36,8 @@ from cognite.client._api.hosted_extractors.sources import SourcesAPI from cognite.client._api.iam import IAMAPI, GroupsAPI, SecurityCategoriesAPI, SessionsAPI, TokenAPI from cognite.client._api.labels import LabelsAPI +from cognite.client._api.postgres_gateway import PostgresGatewaysAPI +from cognite.client._api.postgres_gateway.users import UsersAPI as PostgresUsersAPI from cognite.client._api.raw import RawAPI, RawDatabasesAPI, RawRowsAPI, RawTablesAPI from cognite.client._api.relationships import RelationshipsAPI from cognite.client._api.sequences import SequencesAPI, SequencesDataAPI @@ -147,6 +149,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.hosted_extractors.jobs = MagicMock(spec_set=JobsAPI) self.hosted_extractors.mappings = MagicMock(spec_set=MappingsAPI) + self.postgres_gateway = MagicMock(spec=PostgresGatewaysAPI) + self.postgres_gateway.users = MagicMock(spec_set=PostgresUsersAPI) + self.templates = MagicMock(spec=TemplatesAPI) self.templates.groups = MagicMock(spec_set=TemplateGroupsAPI) self.templates.instances = MagicMock(spec_set=TemplateInstancesAPI) diff --git a/cognite/client/utils/_identifier.py b/cognite/client/utils/_identifier.py index 589d86128..1f0bc4220 100644 --- a/cognite/client/utils/_identifier.py +++ b/cognite/client/utils/_identifier.py @@ -156,6 +156,20 @@ def as_primitive(self) -> str: return self.__value +class Username: + def __init__(self, value: str) -> None: + self.__value: str = value + + def name(self, camel_case: bool = False) -> str: + return "username" + + def as_dict(self, camel_case: bool = True) -> dict[str, str]: + return {self.name(camel_case): self.__value} + + def as_primitive(self) -> str: + return self.__value + + class WorkflowVersionIdentifier: def __init__(self, version: str, workflow_external_id: str) -> None: self.__version: str = version @@ -257,7 +271,7 @@ def unwrap_identifier(identifier: str | int | dict) -> str | int | InstanceId: return identifier["space"] if "instanceId" in identifier: return InstanceId.load(identifier["instanceId"]) - raise ValueError(f"{identifier} does not contain 'id' or 'externalId' or 'space'") + raise ValueError(f"{identifier} does not contain 'id' or 'externalId', or 'space'") @staticmethod def extract_identifiers(dct: dict[str, Any]) -> dict[str, str | int]: @@ -355,6 +369,22 @@ def assert_singleton(self) -> None: raise ValueError("Exactly one user identifier (string) must be specified") +class UsernameSequence(IdentifierSequenceCore[Username]): + @classmethod + def load(cls, usernames: str | SequenceNotStr[str]) -> UsernameSequence: + if isinstance(usernames, str): + return cls([Username(usernames)], is_singleton=True) + + elif isinstance(usernames, Sequence): + return cls([Username(username) for username in usernames], is_singleton=False) + + raise TypeError(f"usernames must be of type str or SequenceNotStr[str]. Found {type(usernames)}") + + def assert_singleton(self) -> None: + if not self.is_singleton(): + raise ValueError("Exactly one username (string) must be specified") + + class WorkflowVersionIdentifierSequence(IdentifierSequenceCore[WorkflowVersionIdentifier]): @classmethod def load(cls, workflow_ids: Sequence[dict]) -> WorkflowVersionIdentifierSequence: diff --git a/docs/source/index.rst b/docs/source/index.rst index 1881c108b..b78ff04b5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -51,6 +51,7 @@ Contents documents data_ingestion hosted_extractors + postgres_gateway data_organization transformations functions diff --git a/docs/source/postgres_gateway.rst b/docs/source/postgres_gateway.rst new file mode 100644 index 000000000..b2f3a670b --- /dev/null +++ b/docs/source/postgres_gateway.rst @@ -0,0 +1,30 @@ +Postgres Gateway +================= +Users API +--------------- +Create Users +^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. automethod:: cognite.client._api.postgres_gateway.UsersAPI.create + +Update Users +^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. automethod:: cognite.client._api.postgres_gateway.UsersAPI.update + +Delete Users +^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. automethod:: cognite.client._api.postgres_gateway.UsersAPI.delete + +Retrieve Users +^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. automethod:: cognite.client._api.postgres_gateway.UsersAPI.retrieve + +List Users +^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. automethod:: cognite.client._api.postgres_gateway.UsersAPI.list + + +User classes +^^^^^^^^^^^^^^^^^^ +.. automodule:: cognite.client.data_classes.postgres_gateway + :members: + :show-inheritance: diff --git a/pyproject.toml b/pyproject.toml index 1112e1da7..a6a11b2cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "cognite-sdk" -version = "7.62.7" +version = "7.62.8" description = "Cognite Python SDK" readme = "README.md" documentation = "https://cognite-sdk-python.readthedocs-hosted.com" diff --git a/scripts/add_capability.py b/scripts/add_capability.py index 1240d5190..45fe9d7b7 100644 --- a/scripts/add_capability.py +++ b/scripts/add_capability.py @@ -10,7 +10,7 @@ def main(client: CogniteClient): new_capabilities = [ { - "hostedExtractorsAcl": { + "postgresGatewayAcl": { "actions": ["READ", "WRITE"], "scope": {"all": {}}, } diff --git a/tests/tests_integration/test_api/test_postgres_gateway/__init__.py b/tests/tests_integration/test_api/test_postgres_gateway/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tests_integration/test_api/test_postgres_gateway/conftest.py b/tests/tests_integration/test_api/test_postgres_gateway/conftest.py new file mode 100644 index 000000000..616c0997b --- /dev/null +++ b/tests/tests_integration/test_api/test_postgres_gateway/conftest.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import pytest + +from cognite.client import CogniteClient +from cognite.client.data_classes.postgres_gateway import SessionCredentials + + +@pytest.fixture +def fresh_credentials(cognite_client: CogniteClient) -> SessionCredentials: + new_session = cognite_client.iam.sessions.create(session_type="ONESHOT_TOKEN_EXCHANGE") + yield SessionCredentials(nonce=new_session.nonce) + cognite_client.iam.sessions.revoke(new_session.id) + + +@pytest.fixture +def another_fresh_credentials(cognite_client: CogniteClient) -> SessionCredentials: + new_session = cognite_client.iam.sessions.create(session_type="ONESHOT_TOKEN_EXCHANGE") + yield SessionCredentials(nonce=new_session.nonce) + cognite_client.iam.sessions.revoke(new_session.id) diff --git a/tests/tests_integration/test_api/test_postgres_gateway/test_users.py b/tests/tests_integration/test_api/test_postgres_gateway/test_users.py new file mode 100644 index 000000000..5d7bd255d --- /dev/null +++ b/tests/tests_integration/test_api/test_postgres_gateway/test_users.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import pytest + +from cognite.client import CogniteClient +from cognite.client.data_classes.postgres_gateway import ( + SessionCredentials, + User, + UserList, + UserUpdate, + UserWrite, +) +from cognite.client.exceptions import CogniteAPIError + + +@pytest.fixture +def one_user(cognite_client: CogniteClient, fresh_credentials: SessionCredentials) -> User: + my_user = UserWrite(credentials=fresh_credentials) + created = cognite_client.postgres_gateway.users.create(my_user) + yield created + cognite_client.postgres_gateway.users.delete(created.username, ignore_unknown_ids=True) + + +class TestUsers: + def test_create_update_retrieve_delete( + self, + cognite_client: CogniteClient, + fresh_credentials: SessionCredentials, + another_fresh_credentials: SessionCredentials, + ) -> None: + my_user = UserWrite(credentials=fresh_credentials) + created: User | None = None + try: + created = cognite_client.postgres_gateway.users.create(my_user) + assert isinstance(created, User) + update = UserUpdate(created.username).credentials.set(another_fresh_credentials) + updated = cognite_client.postgres_gateway.users.update(update) + assert updated.username == created.username + retrieved = cognite_client.postgres_gateway.users.retrieve(created.username) + assert retrieved is not None + assert retrieved.username == created.username + + cognite_client.postgres_gateway.users.delete(created.username) + + with pytest.raises(CogniteAPIError): + cognite_client.postgres_gateway.users.retrieve(created.username) + + cognite_client.postgres_gateway.users.retrieve(created.username, ignore_unknown_ids=True) + + finally: + if created: + cognite_client.postgres_gateway.users.delete(created.username, ignore_unknown_ids=True) + + @pytest.mark.usefixtures("one_user") + def test_list(self, cognite_client: CogniteClient) -> None: + res = cognite_client.postgres_gateway.users.list(limit=1) + assert len(res) == 1 + assert isinstance(res, UserList) diff --git a/tests/tests_unit/test_base.py b/tests/tests_unit/test_base.py index 9d44c54d8..f17d13782 100644 --- a/tests/tests_unit/test_base.py +++ b/tests/tests_unit/test_base.py @@ -37,6 +37,7 @@ from cognite.client.data_classes.datapoints import DatapointsArray from cognite.client.data_classes.events import Event, EventList from cognite.client.data_classes.hosted_extractors import Destination, DestinationList, Source, SourceList +from cognite.client.data_classes.postgres_gateway import User, UserList from cognite.client.exceptions import CogniteMissingClientError from cognite.client.testing import CogniteClientMock from cognite.client.utils import _json @@ -196,7 +197,7 @@ def test_dump_load_only_required( # Hosted extractors does not support the as_write method for cls in all_concrete_subclasses(WriteableCogniteResource) # Hosted extractors does not support the as_write method - if cls not in {Destination} and not issubclass(cls, Source) + if cls not in {Destination, User} and not issubclass(cls, Source) ], ) def test_writable_as_write( @@ -214,7 +215,7 @@ def test_writable_as_write( [ pytest.param(cls, id=f"{cls.__name__} in {cls.__module__}") for cls in all_concrete_subclasses(WriteableCogniteResourceList) - if cls not in [EdgeListWithCursor, NodeListWithCursor, SourceList, DestinationList] + if cls not in {EdgeListWithCursor, NodeListWithCursor, SourceList, DestinationList, UserList} ], ) def test_writable_list_as_write( diff --git a/tests/tests_unit/test_data_classes/test_capabilities.py b/tests/tests_unit/test_data_classes/test_capabilities.py index e261df3c4..ceb884d98 100644 --- a/tests/tests_unit/test_data_classes/test_capabilities.py +++ b/tests/tests_unit/test_data_classes/test_capabilities.py @@ -75,6 +75,7 @@ def all_acls(): {"monitoringTasksAcl": {"actions": ["READ", "WRITE"], "scope": {"all": {}}}}, {"notificationsAcl": {"actions": ["READ", "WRITE"], "scope": {"all": {}}}}, {"pipelinesAcl": {"actions": ["READ", "WRITE"], "scope": {"all": {}}}}, + {"postgresGatewayAcl": {"actions": ["READ", "WRITE"], "scope": {"all": {}}}}, {"projectsAcl": {"actions": ["UPDATE", "LIST", "READ", "CREATE", "DELETE"], "scope": {"all": {}}}}, {"rawAcl": {"actions": ["READ", "WRITE", "LIST"], "scope": {"all": {}}}}, { diff --git a/tests/tests_unit/test_docstring_examples.py b/tests/tests_unit/test_docstring_examples.py index 54ac51b34..91e367c2c 100644 --- a/tests/tests_unit/test_docstring_examples.py +++ b/tests/tests_unit/test_docstring_examples.py @@ -26,6 +26,7 @@ ) from cognite.client._api.data_modeling import containers, data_models, graphql, instances, spaces, views from cognite.client._api.hosted_extractors import destinations, jobs, mappings, sources +from cognite.client._api.postgres_gateway import users as postgres_gateway_users from cognite.client.testing import CogniteClientMock # this fixes the issue with 'got MagicMock but expected Nothing in docstrings' @@ -129,3 +130,6 @@ def test_hosted_extractors(self): run_docstring_tests(sources) run_docstring_tests(destinations) run_docstring_tests(jobs) + + def test_postgres_gateway(self): + run_docstring_tests(postgres_gateway_users) diff --git a/tests/tests_unit/test_utils/test_identifier.py b/tests/tests_unit/test_utils/test_identifier.py index e76422e23..cacceae6d 100644 --- a/tests/tests_unit/test_utils/test_identifier.py +++ b/tests/tests_unit/test_utils/test_identifier.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from cognite.client._constants import MAX_VALID_INTERNAL_ID @@ -7,6 +9,8 @@ InstanceId, UserIdentifier, UserIdentifierSequence, + Username, + UsernameSequence, ) @@ -138,3 +142,31 @@ def test_load_and_dump(self, user_ids, exp_dcts, exp_primitives): def test_load_wrong_type(self): with pytest.raises(TypeError): UserIdentifierSequence.load(123) + + +class TestUsername: + def test_methods(self) -> None: + user_id = Username("foo") + assert user_id.as_primitive() == "foo" + assert user_id.as_dict(camel_case=True) == {"username": "foo"} + assert user_id.as_dict(camel_case=False) == {"username": "foo"} + + +class TestUsernameSequence: + @pytest.mark.parametrize( + "usernames, exp_dcts, exp_primitives", + ( + ("foo", [{"username": "foo"}], ["foo"]), + (["foo", "bar"], [{"username": "foo"}, {"username": "bar"}], ["foo", "bar"]), + ), + ) + def test_load_and_dump( + self, usernames: str | list[str], exp_dcts: dict[str, str], exp_primitives: list[str] + ) -> None: + user_id_seq = UsernameSequence.load(usernames) + assert user_id_seq.as_primitives() == exp_primitives + assert user_id_seq.as_dicts() == exp_dcts + + def test_load_wrong_type(self) -> None: + with pytest.raises(TypeError): + UsernameSequence.load(123)