Skip to content

Commit 07c0a53

Browse files
authored
release v8.0.0a2 (#2435)
2 parents 909e471 + 27116b6 commit 07c0a53

38 files changed

Lines changed: 502 additions & 218 deletions

MIGRATION_GUIDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Changes are grouped as follows:
1616
### Optional
1717
- **Async Support**: The SDK now provides full async support. The main client is now `AsyncCogniteClient`, but the synchronous `CogniteClient` is still available for backward compatibility. An important implementation detail is that it just wraps `AsyncCogniteClient`.
1818
- All helper/utility methods on data classes now have an async variant. A few examples: Class `Asset` has `children` and now also `children_async`, `subtree` and `subtree_async`, class `Function` now has `call` and `call_async`, class `TimeSeries` now has `latest` and `latest_async` etc.
19+
- Instantiating a client has gotten a tiny bit simpler, by allowing either `cluster` or `base_url` to be passed. When passing cluster, it is expected to be on the form 'https://{cluster}.cognitedata.com'
1920
- The context manager `FileMultipartUploadSession`, returned by a call to one of the Files API methods multipart_upload_content` or `multipart_upload_content_session`, now also supports async; you can enter with `async with`, and upload parts using `await upload_part_async`.
2021
- The SDK now ships with a new mock for the async client, namely `AsyncCogniteClientMock`. Both it and the previous `CogniteClientMock` are greatly improved and provide better type safety, checking of call signatures and spec_set=True is now enforced for all APIs (even the mocked client itself), through the use of `create_autospec` and bottom-up construction of nested APIs.
2122
- With the move to an async client, concurrency now works in Pyodide e.g. Jupyter-Lite in the browser. This also means that user interfaces like Streamlit won't freeze while resources from CDF are being fetched!
@@ -38,13 +39,13 @@ Changes are grouped as follows:
3839
- The `__iter__` method has been removed from all APIs. Use `__call__` instead: `for ts in client.time_series()`. This makes it seamless to pass one or more parameters.
3940
- All references to `legacy_name` on time series data classes and API have been removed.
4041
- The helper methods on `client.iam`, `compare_capabilities` and `verify_capabilities` no longer support the `ignore_allscope_meaning` parameter.
41-
- The Files API no longer accepts file handles opened in text mode.
4242
- The method `load_yaml` on the data class `Query` has been removed. Use `load` instead.
4343
- The Templates API has been completely removed from the SDK (the API service has already been shut off)
4444
- The separate beta `CogniteClient` has been removed. Note: APIs currently in alpha/beta are (already) implemented directly on the main client and throw warnings on use.
4545

4646
### Changed
4747
- Attributes on all "read" data classes now have the correct type (typically no longer `Optional[...]`), meaning type inference will be correct. If you try to instantiate these classes directly (*you shouldn't* - use the write versions instead!), you will see that all required parameters in the API response will also be required on the object. **What is a read class?** Any data class returned by the SDK from a call to the API to fetch a resource of some kind.
48+
- All (HTTP) responses from the SDK (returned by e.g. `client.post` or `client.get`) are now of type `CogniteHTTPResponse` (from `cognite.client.response`) instead of the specific type from the underlying http library to support future http-client changes.
4849
- All typed instance apply classes, e.g. `CogniteAssetApply` from `cognite.client.data_classes.data_modeling.cdm.v1` (or `extractor_extensions.v1`) now work with patch updates (using `replace=False`). Previously, all unset fields would be dumped as `None` and thus cleared/nulled in the backend database. Now, any unset fields are not dumped and will not clear an existing value (unless used with `replace=True`).
4950
- When using the Datapoints API to ingest datapoints through `insert_dataframe`, the parameters `external_id_headers` and `instance_id_headers` have been removed. The new logic infers the kind of identifier from the type of the column: an integer is an ID, a string is an external ID and a NodeId (or 2-tuple of space and ext.id) is an instance ID. This also means you can pass more than one type of time series identifier in the same pandas DataFrame.
5051
- Datapoints API method `retrieve_dataframe` and all `to_pandas` methods on datapoints-container-like objects now accept a new parameter: `include_unit` (`bool`). Time series using physical units via `unit_external_id`, will end up as part of the pandas DataFrame columns (like aggregate info).
@@ -59,7 +60,6 @@ Changes are grouped as follows:
5960
- Parameter `partitions` has been removed from all `__call__` methods except for the Raw Rows API (which has special handling for it). It was previosuly being ignored with the added side effect of ignoring `chunk_size` stemming from a very early API design oversight.
6061
- The method `retrieve` on the Workflow Versions API no longer accepts `workflow_external_id` and `version` as separate arguments. Pass a single or a sequence of `WorkflowVersionId` (tuples also accepted).
6162
- When loading a `ViewProperty` or `ViewPropertyApply`, the resource dictionary must contain the `"connectionType"` key or an error is raised.
62-
- The Files API now expects `pathlib.Path` by default, but keeps the `str` support for now.
6363
- The specific exceptions `CogniteDuplicatedError` and `CogniteNotFoundError` should now always be used when appropriate (previously certain API endpoints always used `CogniteAPIError`)
6464
- `ModelFailedException` has changed name to `CogniteModelFailedError`.
6565
- For `class Transformation`, which used to have an async `run` method, this is now named `run_async` to unify the overall interface. The same applies to the `cancel` and `jobs` methods for the same class, and `update` and `wait` on `TransformationJob`.

cognite/client/_api/data_modeling/instances.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,10 +1343,11 @@ async def aggregate(
13431343
Get the average run time in minutes for pumps grouped by release year:
13441344
13451345
>>> from cognite.client import CogniteClient
1346-
>>> from cognite.client.data_classes.data_modeling import ViewId, aggregations as aggs
1346+
>>> from cognite.client.data_classes.aggregations import Average
1347+
>>> from cognite.client.data_classes.data_modeling import ViewId
13471348
>>> client = CogniteClient()
13481349
>>> # async_client = AsyncCogniteClient() # another option
1349-
>>> avg_run_time = aggs.Avg("runTimeMinutes")
1350+
>>> avg_run_time = Average("runTimeMinutes")
13501351
>>> view_id = ViewId("mySpace", "PumpView", "v1")
13511352
>>> res = client.data_modeling.instances.aggregate(view_id, avg_run_time, group_by="releaseYear")
13521353
@@ -1450,10 +1451,11 @@ async def histogram(
14501451
Find the number of people born per decade:
14511452
14521453
>>> from cognite.client import CogniteClient
1453-
>>> from cognite.client.data_classes.data_modeling import aggregations as aggs, ViewId
1454+
>>> from cognite.client.data_classes.aggregations import Histogram
1455+
>>> from cognite.client.data_classes.data_modeling import ViewId
14541456
>>> client = CogniteClient()
14551457
>>> # async_client = AsyncCogniteClient() # another option
1456-
>>> birth_by_decade = aggs.Histogram("birthYear", interval=10.0)
1458+
>>> birth_by_decade = Histogram("birthYear", interval=10.0)
14571459
>>> view_id = ViewId("mySpace", "PersonView", "v1")
14581460
>>> res = client.data_modeling.instances.histogram(view_id, birth_by_decade)
14591461
"""

cognite/client/_api/entity_matching.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323

2424
class EntityMatchingAPI(APIClient):
25+
# TODO: The API class should specify the resource path, not the data class:
2526
_RESOURCE_PATH = EntityMatchingModel._RESOURCE_PATH
2627

2728
async def retrieve(self, id: int | None = None, external_id: str | None = None) -> EntityMatchingModel | None:
@@ -272,8 +273,8 @@ async def predict(
272273
targets (Sequence[dict] | None): entities to match to, does not need an 'id' field. Tolerant to passing more than is needed or used. If omitted, will use data from fit.
273274
num_matches (int): number of matches to return for each item.
274275
score_threshold (float | None): only return matches with a score above this threshold
275-
id (int | None): ids of the model to use.
276-
external_id (str | None): external ids of the model to use.
276+
id (int | None): id of the model to use.
277+
external_id (str | None): external id of the model to use.
277278
278279
Returns:
279280
EntityMatchingPredictionResult: object which can be used to wait for and retrieve results.
@@ -293,17 +294,23 @@ async def predict(
293294
... id=1
294295
... )
295296
"""
296-
297-
model = await self.retrieve(id=id, external_id=external_id)
298-
# TODO: Change assert to proper error
299-
assert model
300-
return await model.predict_async( # could call predict directly but this is friendlier
297+
model = await self._get_model_or_raise(id, external_id)
298+
# TODO: The data class should call the API class 'predict' method, not the other way around:
299+
return await model.predict_async(
301300
sources=EntityMatchingModel._dump_entities(sources),
302301
targets=EntityMatchingModel._dump_entities(targets),
303302
num_matches=num_matches,
304303
score_threshold=score_threshold,
305304
)
306305

306+
async def _get_model_or_raise(self, id: int | None, external_id: str | None) -> EntityMatchingModel:
307+
if id is external_id is None:
308+
raise ValueError("Either id or external_id must be provided.")
309+
model = await self.retrieve(id=id, external_id=external_id)
310+
if model is None:
311+
raise ValueError("No model found with the given identifier(s).")
312+
return model
313+
307314
async def refit(
308315
self,
309316
true_matches: Sequence[dict | tuple[int | str, int | str]],
@@ -332,7 +339,5 @@ async def refit(
332339
>>> true_matches = [(1, 101)]
333340
>>> model = client.entity_matching.refit(true_matches=true_matches, id=1)
334341
"""
335-
model = await self.retrieve(id=id, external_id=external_id)
336-
# TODO: Change assert to proper error
337-
assert model
342+
model = await self._get_model_or_raise(id, external_id)
338343
return await model.refit_async(true_matches=true_matches)

cognite/client/_api/functions/__init__.py

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

33
import ast
4+
import asyncio
45
import importlib
56
import os
67
import re
78
import sys
89
import textwrap
9-
import time
1010
from collections.abc import AsyncIterator, Callable, Sequence
1111
from inspect import getdoc, getsource, signature
1212
from multiprocessing import Process, Queue
@@ -310,7 +310,7 @@ async def _create_function_obj(
310310
file = await self._cognite_client.files.retrieve(id=file_id)
311311
if file and file.uploaded:
312312
break
313-
time.sleep(sleep_time)
313+
await asyncio.sleep(sleep_time)
314314
sleep_time *= 2
315315
else:
316316
raise RuntimeError("Could not retrieve file from files API")

cognite/client/_basic_api_client.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from cognite.client.exceptions import (
2020
CogniteAPIError,
2121
CogniteDuplicatedError,
22+
CogniteHTTPStatusError,
2223
CogniteNotFoundError,
2324
CogniteProjectAccessError,
2425
)
@@ -30,6 +31,7 @@
3031
if TYPE_CHECKING:
3132
from cognite.client import AsyncCogniteClient
3233
from cognite.client.config import ClientConfig
34+
from cognite.client.response import CogniteHTTPResponse
3335

3436

3537
logger = logging.getLogger(__name__)
@@ -45,15 +47,15 @@ class FailedRequestHandler:
4547
headers: dict[str, str] | httpx.Headers
4648
response_headers: dict[str, str] | httpx.Headers
4749
extra: dict[str, Any]
48-
cause: httpx.HTTPStatusError
50+
cause: CogniteHTTPStatusError
4951
stream: bool
5052

5153
def __post_init__(self) -> None:
5254
self.headers = BasicAsyncAPIClient._sanitize_headers(self.headers)
5355
self.response_headers = BasicAsyncAPIClient._sanitize_headers(self.response_headers)
5456

5557
@classmethod
56-
async def from_status_error(cls, err: httpx.HTTPStatusError, stream: bool) -> Self:
58+
async def from_status_error(cls, err: CogniteHTTPStatusError, stream: bool) -> Self:
5759
response = err.response
5860
error, missing, duplicated = {}, None, None
5961

@@ -134,7 +136,7 @@ async def _raise_no_project_access_error(
134136
x_request_id=self.x_request_id,
135137
maybe_projects=maybe_projects,
136138
cluster=cluster,
137-
) from None # we don't surface the underlying httpx.HTTPStatusError
139+
) from None # we don't surface the underlying CogniteHTTPStatusError
138140

139141
def _raise_api_error(self, err_type: type[CogniteAPIError], cluster: str | None, project: str) -> NoReturn:
140142
raise err_type(
@@ -230,7 +232,7 @@ async def _request(
230232
timeout: float | None = None,
231233
include_cdf_headers: bool = False,
232234
api_subversion: str | None = None,
233-
) -> httpx.Response:
235+
) -> CogniteHTTPResponse:
234236
"""
235237
Make a request to something that is outside Cognite Data Fusion, with retry enabled.
236238
Requires the caller to handle errors coming from non-2xx response status codes.
@@ -245,10 +247,10 @@ async def _request(
245247
api_subversion (str | None): When include_cdf_headers=True, override the API subversion to use for the request. Has no effect otherwise.
246248
247249
Returns:
248-
httpx.Response: The response from the server.
250+
CogniteHTTPResponse: The response from the server.
249251
250252
Raises:
251-
httpx.HTTPStatusError: If the response status code is 4xx or 5xx.
253+
CogniteHTTPStatusError: If the response status code is 4xx or 5xx.
252254
"""
253255
http_client = self._select_async_http_client(method in {"GET", "PUT", "HEAD"})
254256
if include_cdf_headers:
@@ -260,7 +262,7 @@ async def _request(
260262
self._log_successful_request(res)
261263
return res
262264

263-
except httpx.HTTPStatusError as err:
265+
except CogniteHTTPStatusError as err:
264266
handler = await FailedRequestHandler.from_status_error(err, stream=False)
265267
handler.log_failed_request()
266268
raise
@@ -278,7 +280,7 @@ async def _stream(
278280
full_headers: dict[str, Any] | None = None,
279281
timeout: float | None = None,
280282
api_subversion: str | None = None,
281-
) -> AsyncIterator[httpx.Response]:
283+
) -> AsyncIterator[CogniteHTTPResponse]:
282284
assert url_path or full_url, "Either url_path or full_url must be provided"
283285
full_url = full_url or resolve_url(self, method, cast(str, url_path))[1]
284286
if full_headers is None:
@@ -293,7 +295,7 @@ async def _stream(
293295
self._log_successful_request(resp, payload=json, stream=True)
294296
yield resp
295297

296-
except httpx.HTTPStatusError as err:
298+
except CogniteHTTPStatusError as err:
297299
await self._handle_status_error(err, payload=json, stream=True)
298300

299301
async def _get(
@@ -304,7 +306,7 @@ async def _get(
304306
follow_redirects: bool = False,
305307
api_subversion: str | None = None,
306308
semaphore: asyncio.BoundedSemaphore | None = None,
307-
) -> httpx.Response:
309+
) -> CogniteHTTPResponse:
308310
_, full_url = resolve_url(self, "GET", url_path)
309311
full_headers = self._configure_headers(additional_headers=headers, api_subversion=api_subversion)
310312
try:
@@ -317,7 +319,7 @@ async def _get(
317319
timeout=self._config.timeout,
318320
semaphore=semaphore,
319321
)
320-
except httpx.HTTPStatusError as err:
322+
except CogniteHTTPStatusError as err:
321323
await self._handle_status_error(err)
322324

323325
self._log_successful_request(res)
@@ -332,7 +334,7 @@ async def _post(
332334
follow_redirects: bool = False,
333335
api_subversion: str | None = None,
334336
semaphore: asyncio.BoundedSemaphore | None = None,
335-
) -> httpx.Response:
337+
) -> CogniteHTTPResponse:
336338
is_retryable, full_url = resolve_url(self, "POST", url_path)
337339
full_headers = self._configure_headers(additional_headers=headers, api_subversion=api_subversion)
338340
# We want to control json dumping, so we pass it along to httpx.Client.post as 'content'
@@ -350,7 +352,7 @@ async def _post(
350352
timeout=self._config.timeout,
351353
semaphore=semaphore,
352354
)
353-
except httpx.HTTPStatusError as err:
355+
except CogniteHTTPStatusError as err:
354356
await self._handle_status_error(err, payload=json)
355357

356358
self._log_successful_request(res, payload=json)
@@ -367,7 +369,7 @@ async def _put(
367369
api_subversion: str | None = None,
368370
timeout: float | None = None,
369371
semaphore: asyncio.BoundedSemaphore | None = None,
370-
) -> httpx.Response:
372+
) -> CogniteHTTPResponse:
371373
_, full_url = resolve_url(self, "PUT", url_path)
372374

373375
full_headers = self._configure_headers(additional_headers=headers, api_subversion=api_subversion)
@@ -384,7 +386,7 @@ async def _put(
384386
timeout=timeout or self._config.timeout,
385387
semaphore=semaphore,
386388
)
387-
except httpx.HTTPStatusError as err:
389+
except CogniteHTTPStatusError as err:
388390
await self._handle_status_error(err, payload=json)
389391

390392
self._log_successful_request(res, payload=json)
@@ -417,15 +419,15 @@ def _refresh_auth_header(self, headers: MutableMapping[str, Any]) -> None:
417419
headers[auth_header_name] = auth_header_value
418420

419421
async def _handle_status_error(
420-
self, error: httpx.HTTPStatusError, payload: dict[str, Any] | None = None, stream: bool = False
422+
self, error: CogniteHTTPStatusError, payload: dict[str, Any] | None = None, stream: bool = False
421423
) -> NoReturn:
422424
"""The response had an HTTP status code of 4xx or 5xx"""
423425
handler = await FailedRequestHandler.from_status_error(error, stream=stream)
424426
handler.log_failed_request(payload)
425427
await handler.raise_api_error(self._cognite_client)
426428

427429
def _log_successful_request(
428-
self, res: httpx.Response, payload: dict[str, Any] | None = None, stream: bool = False
430+
self, res: CogniteHTTPResponse, payload: dict[str, Any] | None = None, stream: bool = False
429431
) -> None:
430432
extra: dict[str, Any] = {
431433
"headers": self._sanitize_headers(res.request.headers),

cognite/client/_cognite_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from cognite.client.utils._auxiliary import load_resource_to_dict
3737

3838
if TYPE_CHECKING:
39-
import httpx
39+
from cognite.client.response import CogniteHTTPResponse
4040

4141

4242
class AsyncCogniteClient:
@@ -94,7 +94,7 @@ def __init__(self, config: ClientConfig | None = None) -> None:
9494

9595
async def get(
9696
self, url: str, params: dict[str, Any] | None = None, headers: dict[str, Any] | None = None
97-
) -> httpx.Response:
97+
) -> CogniteHTTPResponse:
9898
"""Perform a GET request to an arbitrary path in the API."""
9999
return await self._api_client._get(url, params=params, headers=headers)
100100

@@ -104,7 +104,7 @@ async def post(
104104
json: dict[str, Any] | None = None,
105105
params: dict[str, Any] | None = None,
106106
headers: dict[str, Any] | None = None,
107-
) -> httpx.Response:
107+
) -> CogniteHTTPResponse:
108108
"""Perform a POST request to an arbitrary path in the API."""
109109
return await self._api_client._post(url, json=json, params=params, headers=headers)
110110

@@ -114,7 +114,7 @@ async def put(
114114
json: dict[str, Any] | None = None,
115115
params: dict[str, Any] | None = None,
116116
headers: dict[str, Any] | None = None,
117-
) -> httpx.Response:
117+
) -> CogniteHTTPResponse:
118118
"""Perform a PUT request to an arbitrary path in the API."""
119119
return await self._api_client._put(url, json=json, params=params, headers=headers)
120120

0 commit comments

Comments
 (0)