diff --git a/CHANGELOG.md b/CHANGELOG.md index e040e38c28..3e5279b50a 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.6.0] - 2023-12-13 +### Added +- Support for querying data models through graphql. See `client.data_modeling.graphql.query`. + ## [7.5.7] - 2023-12-12 ### Fixed - Certain combinations of `start`/`end` and `granularity` would cause `retrieve_dataframe_in_tz` to raise due to diff --git a/cognite/client/_api/data_modeling/graphql.py b/cognite/client/_api/data_modeling/graphql.py index 52dec1c4d3..d5a6683c55 100644 --- a/cognite/client/_api/data_modeling/graphql.py +++ b/cognite/client/_api/data_modeling/graphql.py @@ -8,6 +8,7 @@ from cognite.client.data_classes.data_modeling.graphql import DMLApplyResult from cognite.client.data_classes.data_modeling.ids import DataModelId from cognite.client.exceptions import CogniteGraphQLError, GraphQLErrorSpec +from cognite.client.utils._auxiliary import interpolate_and_url_encode class DataModelingGraphQLAPI(APIClient): @@ -133,3 +134,32 @@ def apply_dml( query_name = "upsertGraphQlDmlVersion" res = self._post_graphql(url_path="/dml/graphql", query_name=query_name, json=payload) return DMLApplyResult.load(res[query_name]["result"]) + + def query(self, id: DataModelIdentifier, query: str, variables: dict[str, Any] | None = None) -> dict[str, Any]: + """Execute a GraphQl query against a given data model. + + Args: + id (DataModelIdentifier): The data model to query. + query (str): The query to issue. + variables (dict[str, Any] | None): An optional dict of variables to pass to the query. + + Returns: + dict[str, Any]: The query result + + Examples: + + Execute a graphql query against a given data model:: + + >>> from cognite.client import CogniteClient + >>> c = CogniteClient() + >>> res = c.data_modeling.graphql.query( + ... id=("mySpace", "myDataModel", "v1"), + ... query="listThings { items { thingProperty } }", + ... ) + """ + dm_id = DataModelId.load(id) + endpoint = interpolate_and_url_encode( + "/userapis/spaces/{}/datamodels/{}/versions/{}/graphql", dm_id.space, dm_id.external_id, dm_id.version + ) + res = self._post_graphql(url_path=endpoint, query_name="", json={"query": query, "variables": variables}) + return res diff --git a/cognite/client/_version.py b/cognite/client/_version.py index 821a3f860c..0cad9e18cb 100644 --- a/cognite/client/_version.py +++ b/cognite/client/_version.py @@ -1,4 +1,4 @@ from __future__ import annotations -__version__ = "7.5.7" +__version__ = "7.6.0" __api_subversion__ = "V20220125" diff --git a/cognite/client/data_classes/data_modeling/graphql.py b/cognite/client/data_classes/data_modeling/graphql.py index 628fb13fe0..d32f06aa88 100644 --- a/cognite/client/data_classes/data_modeling/graphql.py +++ b/cognite/client/data_classes/data_modeling/graphql.py @@ -40,3 +40,12 @@ def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = created_time=resource["createdTime"], last_updated_time=resource["lastUpdatedTime"], ) + + +@dataclass +class GraphQlQueryResult(CogniteObject): + items: list[dict[str, Any]] + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls(resource["items"]) diff --git a/docs/source/data_modeling.rst b/docs/source/data_modeling.rst index 296865bfbd..82d6277654 100644 --- a/docs/source/data_modeling.rst +++ b/docs/source/data_modeling.rst @@ -260,3 +260,7 @@ GraphQL Apply DML ^^^^^^^^^ .. automethod:: cognite.client._api.data_modeling.graphql.DataModelingGraphQLAPI.apply_dml + +Execute GraphQl query +^^^^^^^^^^^^^^^^^^^^^ +.. automethod:: cognite.client._api.data_modeling.graphql.DataModelingGraphQLAPI.query diff --git a/pyproject.toml b/pyproject.toml index bf96530e46..433b59e7bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "cognite-sdk" -version = "7.5.7" +version = "7.6.0" description = "Cognite Python SDK" readme = "README.md" documentation = "https://cognite-sdk-python.readthedocs-hosted.com" diff --git a/tests/tests_integration/test_api/test_data_modeling/test_graphql.py b/tests/tests_integration/test_api/test_data_modeling/test_graphql.py index 0e0f6fef0b..8d549345b1 100644 --- a/tests/tests_integration/test_api/test_data_modeling/test_graphql.py +++ b/tests/tests_integration/test_api/test_data_modeling/test_graphql.py @@ -3,7 +3,12 @@ import pytest from cognite.client import CogniteClient -from cognite.client.data_classes.data_modeling import DataModel, DataModelApply, DataModelId, Space +from cognite.client.data_classes.data_modeling import ( + DataModel, + DataModelApply, + DataModelId, + Space, +) from cognite.client.exceptions import CogniteGraphQLError @@ -14,6 +19,15 @@ def data_model(cognite_client: CogniteClient, integration_test_space: Space) -> ) +@pytest.fixture(scope="session") +def data_model_for_query_test(cognite_client: CogniteClient, integration_test_space: Space) -> DataModel: + data_model = cognite_client.data_modeling.data_models.apply( + DataModelApply(integration_test_space.space, "DataModelForGraphQlQueryTest", "1") + ) + cognite_client.data_modeling.graphql.apply_dml(data_model.as_id(), "type Thing { someProp: String! }") + return data_model + + class TestDataModelingGraphQLAPI: def test_apply_dml(self, cognite_client: CogniteClient, data_model: DataModel) -> None: dml = "type SomeType { someProp: String! } type AnotherType { anotherProp: String! }" @@ -64,3 +78,64 @@ def test_wipe_and_regenerate_dml(self, cognite_client: CogniteClient, data_model } """ assert res.strip() == textwrap.dedent(expected).strip() + + def test_query(self, cognite_client: CogniteClient, data_model_for_query_test: DataModel) -> None: + query = """ + { + listThing { + items { + externalId + space + someProp + } + } + } + """ + res = cognite_client.data_modeling.graphql.query(data_model_for_query_test.as_id(), query) + assert res == {"listThing": {"items": []}} + + def test_query_with_intent(self, cognite_client: CogniteClient, data_model_for_query_test: DataModel) -> None: + query = """ + query MyQuery { + listThing { + items { + externalId + space + someProp + } + } + } + """ + res = cognite_client.data_modeling.graphql.query(data_model_for_query_test.as_id(), query) + assert res == {"listThing": {"items": []}} + + def test_query_with_variables(self, cognite_client: CogniteClient, data_model_for_query_test: DataModel) -> None: + query = """ + query MyQuery($first: Int) { + listThing(first: $first) { + items { + externalId + space + someProp + } + } + } + """ + res = cognite_client.data_modeling.graphql.query( + data_model_for_query_test.as_id(), query, variables={"first": 10} + ) + assert res == {"listThing": {"items": []}} + + def test_query_with_error(self, cognite_client: CogniteClient, data_model_for_query_test: DataModel) -> None: + query = """ + { + listThing { + items { + i_dont_exist + } + } + } + """ + + with pytest.raises(CogniteGraphQLError, match="Field 'i_dont_exist' in type 'Thing' is undefined"): + cognite_client.data_modeling.graphql.query(data_model_for_query_test.as_id(), query)