feat: overhaul archive search logic across client#648
Conversation
|
| Filename | Overview |
|---|---|
| nominal/core/_utils/query_tools.py | Introduces ArchiveStatusFilter enum and resolve_effective_archive_status, centralising all deprecated-arg resolution. Backfill helpers correctly construct OR-clauses for query-side archive filtering. Edge-case conflicts between deprecated args are partially handled (new-vs-deprecated is guarded; deprecated-vs-deprecated falls through silently—already flagged in prior threads). |
| nominal/core/client.py | Broadly correct archive-status threading through all search methods, but create_or_find_asset silently narrowed its search scope from "all workspaces" to "default workspace" for unpinned clients, which can cause duplicate-asset creation or NominalConfigError regressions. |
| nominal/core/_utils/pagination_tools.py | Clean refactor: all paginated search helpers now accept archive_status and thread it into archived_statuses on the request object. Workbook/template helpers correctly defer to query-side filtering with a documenting TODO. |
| nominal/core/_clientsbunch.py | Introduces LazyField-backed resolve_default_workspace_rid and resolve_workspace helpers. Thread-safety is achieved via Lock inside LazyField.get_or_init; the lockless is_initialized() / get() read path in resolve_workspace is safe because _value is irreversibly set. |
| nominal/_utils/dataclass_tools.py | New LazyField[T] class provides thread-safe lazy initialisation with Lock. get_or_init is atomic; get and is_initialized are lock-free but safe since _value only transitions from _UNSET to a non-_UNSET value. |
| nominal/_utils/deprecation_tools.py | Adds _NotProvided sentinel class. The positional-arg detection branch (len(args) > len(param_names) - 1) is noted with a TODO because all deprecated kwargs in this PR are keyword-only and can't actually be passed positionally—this branch is dead code in current usage. |
| nominal/core/workbook.py | Correctly migrates _search_workbooks and _iter_search_workbooks to archive_status; removes the now-unused include_archived/archived params from internal helpers. Workbook pagination always requests archived+draft and relies on the query clause for filtering. |
| nominal/core/asset.py | Correctly deprecates include_archived, adds archive_status, and wires resolve_effective_archive_status into Asset.search_workbooks. Also migrates workspace_rid accesses to resolve_default_workspace_rid(). |
| nominal/core/run.py | Mirrors the asset.py pattern for Run.search_workbooks: deprecates include_archived, adds archive_status, and delegates resolution to resolve_effective_archive_status. |
| tests/e2e/test_search.py | Substantial new coverage: ArchiveSearchContext fixture creates paired active/archived resources, and _assert_archive_status_behavior / _assert_include_archived_behavior helpers verify all three filter values and the deprecation warning path. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["Public search method\n(e.g. search_runs, search_workbooks)"] --> B{Deprecated args\npresent?}
B -- "archive_status + legacy arg" --> C["ValueError raised"]
B -- "include_archived=True" --> D["ArchiveStatusFilter.ANY"]
B -- "archived=True" --> E["ArchiveStatusFilter.ARCHIVED"]
B -- "neither / False values" --> F["ArchiveStatusFilter.NOT_ARCHIVED\n(default)"]
B -- "archive_status provided" --> G["Use archive_status directly"]
D --> H{Backend\nsupports\narchived_statuses?}
E --> H
F --> H
G --> H
H -- "Yes (runs, assets, events,\nchecklists, secrets, videos)" --> I["Pass archived_statuses list\nto paginated request"]
H -- "No (datasets, workbooks,\ntemplates — backfill)" --> J["_backfill_*_archive_query_clause()\nAdds OR/AND query clause\nfor archive status"]
I --> K["paginate_rpc → server-side filter"]
J --> L["paginate_rpc → query-side filter\n(show_archived=True for workbooks)"]
Comments Outside Diff (1)
-
nominal/core/client.py, line 1181 (link)create_or_find_assetsearch scope regression for unpinned clientsThe old code passed
self._clients.workspace_riddirectly tosearch_assets. When the client has no explicitly pinned workspace (workspace_rid=None), thatNonepropagated throughsearch_assets(workspace=None)→workspace or WorkspaceSearchType.ALL→ a search across all workspaces.With the new code,
WorkspaceSearchType.DEFAULTis always used, which callsresolve_default_workspace_rid()and scopes the search to a single workspace. For unpinned clients (no configuredworkspace_rid) this causes two behavioral regressions:- Duplicate assets can be silently created — if an existing asset with the same properties lives in a non-default workspace, it won't be found, and a new duplicate will be created in the default workspace.
- Hard failure where there was none — if no default workspace can be resolved,
create_or_find_assetnow raisesNominalConfigError, whereas before it returned results from the global search.
For pinned clients the behaviour is unchanged (both paths resolve to the same workspace RID). Consider keeping the old fallback for unpinned clients, or documenting that a workspace must be configured for
create_or_find_assetto work correctly:# Preserve old all-workspaces fallback when no workspace is pinned: search_workspace = WorkspaceSearchType.DEFAULT if self._clients.workspace_rid else WorkspaceSearchType.ALL assets = self.search_assets(properties=properties, workspace=search_workspace)
Last reviewed commit: 0581daf
5611231 to
0581daf
Compare
0581daf to
38ee33b
Compare
38ee33b to
45f6e1f
Compare
seanmreidy
left a comment
There was a problem hiding this comment.
One question about type hint additions
51f154b to
d66def8
Compare
d66def8 to
676e992
Compare
|
@claude do a review again |
|
Claude finished @drake-nominal's task —— View job Code Review for PR #648I've performed a comprehensive analysis of this PR that overhauls archive-aware search across the SDK. Here's my detailed review: ✅ Overall AssessmentThis is a well-structured and comprehensive refactor that successfully centralizes archive status filtering across the client SDK. The implementation is consistent, thoroughly tested, and maintains backward compatibility through proper deprecation patterns. 🔍 Key Strengths1. Clean Architecture
2. Backward Compatibility
3. Comprehensive Test Coverage
|
Summary
Revamps archive-aware search across the SDK by introducing a shared
ArchiveStatusFilterflow, threading it through public search APIs, and adding end-to-end coverage for the new behavior.What changed
ArchiveStatusFilterenum and exported it fromnominal.core.archive_statussupport to search APIs that previously only returned non-archived results:NominalClient.search_datasetsNominalClient.search_secretsNominalClient.search_videosNominalClient.search_runsNominalClient.search_checklistsNominalClient.search_assetsNominalClient.search_eventsNominalClient.search_workbooksNominalClient.search_workbook_templatesAsset.search_workbooksRun.search_workbooks_NotProvided+@warn_on_deprecated_argumentpattern:archivedinclude_archivedresolve_effective_archive_status(...)so the archive-status mapping and conflict handling live in one place.Test coverage
archive_statuson top-level public search methods.archive_statusand deprecatedinclude_archivedon:NominalClient.search_workbooksAsset.search_workbooksRun.search_workbooksTesting
pytest tests/e2e/test_search.py --profile stagingruff check testsmypy tests