Skip to content

feat(records): aggregate endpoint#2705

Open
andersfylling wants to merge 24 commits into
masterfrom
andersfylling/records/aggregate-pr1
Open

feat(records): aggregate endpoint#2705
andersfylling wants to merge 24 commits into
masterfrom
andersfylling/records/aggregate-pr1

Conversation

@andersfylling

@andersfylling andersfylling commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Description

Introduces support for the aggregate endpoint for the Records collection.

andersfylling and others added 2 commits June 25, 2026 10:49
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@andersfylling andersfylling changed the title feat(records): add aggregate endpoint feat(records): aggregate endpoint Jun 30, 2026
andersfylling and others added 3 commits June 30, 2026 10:53
Resolve conflicts with filter/sync PRs (#2698, #2699). Reuse
TimeRange and RecordTargetUnits in the aggregate endpoint for
consistency with filter and sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eneric base tests

Aggregate request builders raise NotImplementedError in _load and
result classes have non-standard constructors, so they cannot
participate in the generic dump/load round-trip tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.74619% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.72%. Comparing base (8b99b98) to head (26cb596).

Files with missing lines Patch % Lines
...gnite/client/data_classes/data_modeling/records.py 99.59% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2705      +/-   ##
==========================================
+ Coverage   93.71%   93.72%   +0.01%     
==========================================
  Files         504      504              
  Lines       51294    51680     +386     
==========================================
+ Hits        48070    48439     +369     
- Misses       3224     3241      +17     
Files with missing lines Coverage Δ
cognite/client/_api/data_modeling/records.py 97.50% <100.00%> (+0.48%) ⬆️
cognite/client/_sync_api/data_modeling/records.py 100.00% <100.00%> (ø)
...nite/client/data_classes/data_modeling/__init__.py 100.00% <ø> (ø)
...s_unit/test_api/test_data_modeling/test_records.py 100.00% <100.00%> (ø)
tests/tests_unit/test_base.py 98.94% <100.00%> (+<0.01%) ⬆️
...gnite/client/data_classes/data_modeling/records.py 99.24% <99.59%> (+1.17%) ⬆️

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

andersfylling and others added 4 commits June 30, 2026 12:49
…aggregate classes from base tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…perty

Encapsulates the parsed bucket list behind a private attribute so callers
can't mutate cached state, and drops the redundant one-line docstrings
that only restated the class names.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@andersfylling
andersfylling marked this pull request as ready for review July 12, 2026 20:23
@andersfylling
andersfylling requested review from a team as code owners July 12, 2026 20:23

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces record aggregation capabilities to the data modeling records API, adding async and sync aggregate methods alongside various typed aggregate request builders and result classes. The review feedback suggests refactoring RecordsAggregate to not inherit from CogniteResource since it functions as a request builder rather than a DTO, which will also simplify unit test exclusions. Additionally, it is recommended to use a more specific type hint for the aggregates parameter to avoid the use of Any.

Comment thread cognite/client/data_classes/data_modeling/records.py Outdated
Comment thread cognite/client/_api/data_modeling/records.py Outdated
>>> res = client.data_modeling.records.aggregate(
... stream_id="my-stream",
... aggregates={
... "avg_temp": Avg(["my-space", "sensor", "temperature"]),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make it more clear that this is a property reference.

Ref docs:

the first segment is the space that the container belongs to,
the second segment is the container external id,
the third segment is the property id inside the container.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

specified the property key in the examples

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep these out of cognite/client/data_classes/data_modeling/__init__.py and only in ...records.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a heads up it will be inconsistent with the other data classes that are accessible there. Do you want purely aggregate related classes, or all records/streams classes to be in their own module?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've prefixed the response agg types with records, and then i moved the builder types into the records module. Do you want me to move the response types into records as well?


Examples:

Aggregate average temperature using typed helpers:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder a bit about these examples - they feel like examples of what kind of data that belongs in the Datapoints service, not the Records service. Could you ask Claude to come up with different examples? 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-used the examples from the cognite api docs

Comment thread cognite/client/data_classes/data_modeling/records.py Outdated
Comment on lines +76 to +78
@classmethod
def _load(cls, resource: dict[str, Any]) -> Self:
raise NotImplementedError(f"{cls.__name__} is a request builder and cannot be loaded from API responses")

@haakonvt haakonvt Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In v7 of the SDK, load was made public so that all request and response classes could be dumped (and loaded back) into config files, thus the _load method here need to be supported.

Comment on lines +366 to +373
for result_cls in (
UniqueValuesAggregateResult,
NumberHistogramAggregateResult,
TimeHistogramAggregateResult,
FilterAggregateResult,
):
if result_cls._buckets_key in resource:
return result_cls._load(resource)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here, I would rather have you build a mapping as a module-level constant for constant-time lookup

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +376 to +380
def __init__(self, raw_result: dict[str, Any]) -> None:
self._raw_result = raw_result

def dump(self, camel_case: bool = True) -> dict[str, Any]:
return self._raw_result

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seem a bit lazy tbh 😆 This camel_case param is being ignored

Comment on lines +399 to +413
def __init__(self, raw_bucket: dict[str, Any]) -> None:
self._raw_bucket = raw_bucket
self.count = raw_bucket["count"]
self.value = raw_bucket.get("value")
self.interval_start = raw_bucket.get("intervalStart")
self.aggregates = raw_bucket.get("aggregates", {})
self.results = {
aggregate_id: RecordsAggregateResult._load(result)
for aggregate_id, result in self.aggregates.items()
if isinstance(result, dict)
}

@classmethod
def _load(cls, resource: dict[str, Any]) -> Self:
return cls(resource)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This init looks like load

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Args:
aggregates (dict[str, Any]): Aggregate results keyed by the client-defined aggregate IDs.
typing (TypeInformation | None): Optional property typing metadata.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels weird to me to have this typing info randomly put here, but I guess you are just mimicking DMS? Should we consider dropping that from /aggregate until someone requests it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's part of the documentation, so lets keep it so users get a familiar sense when using the python sdk.

image

},
}

def test_records_aggregation_dump_round_trip(self) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested automatically, unless you opted out

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return {"property": self.property}


class Avg(_PropertyAggregate):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we can't reuse the existing classes in data_classes/aggregations?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those seem to be designed around views, and have different logic for refering to a property. We also have aggregates that aren't handled there++. So to me it got a bit messy, and I went with our own. Though I hope we can consolidate over time.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those seem to be designed around views

There's no mention of views there, so not sure what you're referring to.

have different logic for refering to a property.

I thought we were referencing properties in the same way everywhere? What's the deviation?

We also have aggregates that aren't handled there

You can add new aggregates to that module

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants