Add column groups support - #345
Conversation
Make the block model column-group API reflect the service instead of adding a bare-title abstraction layer: - Callers key `data` by each column's exact upload heading (bare for ungrouped, qualified `group▸leaf` for grouped); the SDK uploads the table verbatim and never renames columns. - `column_groups` maps a new column's qualified heading → group, or an existing column's current qualified title → new group (`""` ungroups). - `update_columns` / `delete_columns` use the column's current stored title (qualified if grouped, bare otherwise). Removed the hidden latest-version fetch that translated bare → qualified titles. - Add opt-in `qualified_heading` and `qualify_headings` helpers (and a `Table.rename_columns` protocol method) to build qualified headings from bare-titled data, re-exported from the package root. - Update docstrings and rewrite/extend tests for the new contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rohancrookbain-seequent
left a comment
There was a problem hiding this comment.
Partially reviewed; will have a more complete look soon!
There was a problem hiding this comment.
Pull request overview
Adds column-group lifecycle and column assignment/move support to the evo.blockmodels high-level client, along with new public data models and helpers for qualified column headings (preview-gated via API-Preview: opt-in).
Changes:
- Adds group CRUD/update support via
BlockModelAPIClient.update_groups(...)and wiresgroupsinto the update payload. - Adds
column_groupssupport for new columns and for moving/ungrouping existing columns during data re-upload. - Introduces qualified-title utilities and Version group-lookup helpers, plus expanded test coverage.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/evo-blockmodels/tests/test_update.py | Adds tests for grouped column creation, group moves/ungrouping via re-upload, and update_groups behavior. |
| packages/evo-blockmodels/tests/test_group_helpers.py | New tests covering Version group helper methods and extra-field validation and heading qualification helpers. |
| packages/evo-blockmodels/src/evo/blockmodels/data.py | Adds group input models, qualified-title helpers, and Version group bridge helpers. |
| packages/evo-blockmodels/src/evo/blockmodels/client.py | Implements group update endpoint support and column group assignment/move logic with validation and payload shaping. |
| packages/evo-blockmodels/src/evo/blockmodels/_types.py | Extends Table protocol with rename_columns(...) to support heading qualification helper. |
| packages/evo-blockmodels/src/evo/blockmodels/init.py | Re-exports qualified-title helpers and separator at package root. |
| packages/evo-blockmodels/pyproject.toml | Bumps package version to 0.7.0. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
Preview-only group functionality is documented but not consistently enforced in the client (and one test currently exercises group updates without preview opt-in), which can lead to runtime API failures and contract mismatches.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
packages/evo-blockmodels/src/evo/blockmodels/client.py:912
- Same as
_add_new_columns():_update_columns()can receive a non-emptycolumn_groupsmapping whenpreview=False, but will send the payload without the preview opt-in header. Guard early to match the documented preview contract and avoid hard-to-debug API errors.
if column_groups is None:
column_groups = {}
data_type_map = {name: data_type for name, data_type in zip(schema.names, schema.types)}
packages/evo-blockmodels/tests/test_update.py:967
- After switching to the preview client for ungrouping, this assertion should also include the preview opt-in header; otherwise the test will accept behavior that contradicts the preview-only contract for column groups.
method=RequestMethod.PATCH,
path=f"{self.base_path}/block-models/{BM_UUID}/blocks",
body=expected_update_body.model_dump(mode="json", exclude_unset=True),
headers=DEFAULT_EXPECTED_HEADERS,
- Files reviewed: 7/7 changed files
- Comments generated: 5
- Review effort level: Lite
mikecowie-seequent
left a comment
There was a problem hiding this comment.
One meaningful spot
Description
Adds full column-group support to the high-level
BlockModelAPIClientfor the block model service. Previously the SDK could only read groups off a version; callers had to drop down to the generated endpoint layer to create, assign, or modify them. This change exposes the complete group lifecycle — create, assign, read, update and delete — through the public client and data model.Column groups are a preview feature: the client must be constructed with
preview=Trueto use them, consistent with column tags. (This is no longer required, this will be merged after the feature is out of API-Preview.)The design goal is for the SDK to mirror the service: where the service addresses columns by their qualified (
group▸title) title, the caller supplies that same qualified title. The SDK does not invent a plain-title abstraction, rename your data, or perform hidden version lookups — this avoids ambiguity when the same title is reused across groups.Write path
BlockModelAPIClient.update_groups(bm_id, new=, update=, delete=, comment=). Create groups withGroupDefinition(title, parent group, missing-column policy, tags, hidden flag); rename, re-parent, change policy, replace tags or toggle visibility withGroupMetadataUpdate; delete by title. Any combination can be applied in a single call.column_groupsparameter onadd_new_columns,add_new_subblocked_columns,update_block_model_columnsandupdate_subblocked_columnsmaps a new column's qualified title (its key indata, e.g.Assays▸Cu) → the group it belongs to (Assays). The SDK derives the title by stripping the group prefix.update_column_metadatawithColumnMetadataUpdate(group=...). A column's group is metadata, so moving it needs no data upload. Address the column by its current qualified title and setgroupto the new group's qualified title (or""to ungroup).Custom qualified-title separator
The character that joins a group's title to a column's title in a qualified title defaults to
▸(QUALIFIED_TITLE_SEPARATOR). A block model may use a different single-character separator, so both the write and read paths accept an optionalseparatorargument:add_new_columns,add_new_subblocked_columns,update_block_model_columns,update_subblocked_columns.query_block_model_to_cache,query_block_model_as_table.On every one of these,
separatoris used to parse the qualified titles the caller supplies and is forwarded to the service (asqualified_title_separator) for that request; on the read path it also governs how returned qualified headers are rendered. The default is never sent on the wire, so existing behaviour is unchanged; the argument is only serialised when a non-default separator is provided. This matters because a model written with a non-default separator (whose stored titles may legitimately contain▸) can only be queried back by declaring the same separator — otherwise the service rejects the request.Column titles
The caller keys
databy each column's title: a plain title for an ungrouped column, or the qualifiedgroup▸titlefor a grouped one. The SDK uploads the table verbatim and never renames columns. Two opt-in helpers are provided to build these titles from plain-titled data:get_qualified_title(group, title)— builds a single qualified title.qualify_column_titles(data, {title: group})— renames a plain-titled table and returns(renamed_table, column_groups)ready to pass straight to the client.Read path
Version/ListingVersiongain bridge helpers so a caller who wrote a group by title can find it again on the returned version without hand-rolling a lookup:group_by_uuid,group_for_column,qualified_group_titleandgroup_by_qualified_title.Public API
evo.blockmodels.data:GroupDefinitionGroupMetadataUpdateMissingColumnPolicyQUALIFIED_TITLE_SEPARATORget_qualified_titleandqualify_column_titlesget_qualified_title,qualify_column_titlesandQUALIFIED_TITLE_SEPARATORare also re-exported from theevo.blockmodelspackage root.Notes
update_columns/delete_columns) are addressed by the exact title the service currently stores them under: the qualifiedgroup▸titleif grouped, or the plain title if not. There is no hidden lookup — the caller states the current reference.update_column_metadata;ColumnMetadataUpdateaccepts agroupfield (""ungroups). Thecolumn_groupsparameter on the column methods only groups new columns.create_block_model(none exist yet); the documented flow is create the model →update_groups(new=...)→ add columns withcolumn_groups.GroupDefinition,GroupMetadataUpdate,ColumnMetadataUpdate) reject unknown fields to catch typos early.get_qualified_title/qualify_column_titleshelpers, per-requestgroup_missing_column_override, the customseparator(parsing qualified titles and forwardingqualified_title_separator, default omitted), preview-header propagation, input validation, and all bridge helpers.QUALIFIED_TITLE_SEPARATOR(▸) is only the default. Callers on a non-default separator pass it explicitly viaseparatoron both the write and read methods; the constant is never hard-coded internally, so those callers are not broken. The generatedUpdateDataLiteandQueryCriteriaendpoint models carry thequalified_title_separatorfield.Acceptance criteria
BlockModelAPIClient.qualified_title_separator; the default▸is never sent on the wire. A model written with a non-default separator can be queried back by declaring the same separator.get_qualified_title,qualify_column_titles,QUALIFIED_TITLE_SEPARATOR) are available fromevo.blockmodels.dataand the package root.Checklist