Skip to content

Commit a8b7f19

Browse files
authored
Feature/20260115 improvements (#402)
* fix(parameters): correct parameter value removal and add rename_parameter_context - Fix prepare_parameter to properly unset parameter values using value_removed flag - Previously, passing None or omitting value could result in empty string or null instead of actually removing the parameter value - Add _NOT_PROVIDED sentinel to distinguish omitted value from explicit None - prepare_parameter now correctly handles: omit (preserve existing), None (remove), empty string (set to ""), or value (set) - Add rename_parameter_context() for lightweight context renaming - Use config.long_max_wait for update_parameter_context timeout - Include description in get_parameter_ownership_map output - Add comprehensive tests for new functionality * feat(bulletins): add descendants parameter to get_bulletin_board - Add descendants=True parameter to filter bulletins by process group hierarchy - When True (default), includes bulletins from all child process groups - When False, only returns bulletins from components directly in the specified PG - Builds regex pattern of all PG IDs for efficient API filtering * feat(canvas): add name lookup support to list_all_controllers - Add greedy and identifier_type parameters for flexible PG resolution - Supports UUID, name, or ProcessGroupEntity as pg_id input - Uses resolve_entity for consistent behavior with other canvas functions * feat(ci): enhance get_status with throughput stats and descendant bulletins - Add flowfiles_in/out, bytes_in/out, bytes_read/written stats - Use descendants=True for bulletin collection to match processor/controller scope - Update docstring to document bulletin counts * perf(ci): batch parameter updates to reduce cycle overhead - Batch all parameters per context into single update_parameter_context call - Avoids multiple stop/disable/update/enable/restart cycles per parameter - Preserve parameter descriptions during updates - Add tests for empty string and None value handling * docs(layout): add PORT_QUEUE_BOX_WIDTH constant - Document that port-to-port connections use larger queue boxes (240 vs 224) - Clarify existing QUEUE_BOX_WIDTH is for processor-to-processor connections * feat(cli): standardize error field convention for non-zero exit codes - CLI now exits with code 1 when result dict contains 'error' or 'errors' key - Update cleanup.py: change 'message' to 'error' for failure cases - Update verify_config.py: add 'error' key with failed component names - Add tests for error key detection and exit code behavior - Document error field convention in cli.rst and ci.rst for CI authors This enables scripts to rely on exit codes for operational failures, not just Python exceptions. CI functions should include an 'error' key when operations fail to trigger non-zero exit. * docs: add 1.4.0 release notes to history
1 parent 944d82a commit a8b7f19

File tree

16 files changed

+721
-55
lines changed

16 files changed

+721
-55
lines changed

docs/ci.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,39 @@ CI operations are designed for automation scenarios:
1515
- **Sensible defaults**: Minimal configuration required for common use cases
1616
- **Structured output**: Returns plain dicts suitable for CI artifact formats
1717
- **Error handling**: Raises exceptions with clear error messages
18+
- **Exit code support**: Operations that fail include an ``error`` key, causing the CLI to exit with code 1
19+
20+
Error Field Convention
21+
----------------------
22+
23+
CI functions follow a standard convention for indicating operational failures:
24+
25+
- **Success**: Return a dict without ``error`` or ``errors`` keys
26+
- **Failure**: Include an ``error`` (string) or ``errors`` (string) key with a description
27+
28+
This enables scripts to rely on exit codes:
29+
30+
.. code-block:: bash
31+
32+
# Exit code will be 1 if verification fails
33+
if nipyapi ci verify_config --process_group_id "$PG_ID"; then
34+
nipyapi ci start_flow --process_group_id "$PG_ID"
35+
else
36+
echo "Verification failed"
37+
exit 1
38+
fi
39+
40+
When writing custom CI functions, follow this convention:
41+
42+
.. code-block:: python
43+
44+
def my_ci_operation(...) -> dict:
45+
# On success - no error key
46+
if success:
47+
return {"status": "complete", "count": 5}
48+
49+
# On failure - include error key
50+
return {"status": "failed", "error": "Operation failed: reason"}
1851
1952
Quick Start
2053
===========

docs/cli.rst

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,9 @@ Usage Examples
341341
Error Handling
342342
==============
343343

344-
The CLI returns structured error responses on failure:
344+
The CLI returns structured error responses and uses non-zero exit codes for failures.
345+
346+
**Exception errors** (Python exceptions during execution):
345347

346348
.. code-block:: json
347349
@@ -353,10 +355,44 @@ The CLI returns structured error responses on failure:
353355
"logs": ["nipyapi.canvas: Fetching process group..."]
354356
}
355357
358+
**Operational errors** (command completed but operation failed):
359+
360+
CI functions include an ``error`` or ``errors`` field when an operation fails.
361+
The CLI detects these fields and exits with code 1.
362+
363+
.. code-block:: json
364+
365+
{
366+
"verified": "false",
367+
"failed_count": 2,
368+
"error": "Verification failed for: DBCPConnectionPool, ExecuteSQL"
369+
}
370+
356371
Exit codes:
357372

358-
- ``0``: Success
359-
- ``1``: Error (check output for details)
373+
- ``0``: Success (no ``error``/``errors`` key in result)
374+
- ``1``: Error (exception raised, or result contains ``error``/``errors`` key)
375+
376+
Error Field Convention
377+
----------------------
378+
379+
When writing custom CI functions or extending nipyapi, follow this convention:
380+
381+
- Include an ``error`` key (string) when an operation fails
382+
- Use ``errors`` key (string) for multiple error messages (pipe-separated)
383+
- Do NOT include ``error``/``errors`` keys on success
384+
385+
This ensures the CLI exits with the correct code for scripting:
386+
387+
.. code-block:: bash
388+
389+
# Script can rely on exit code
390+
if nipyapi ci verify_config --process_group_id "$PG_ID"; then
391+
nipyapi ci start_flow --process_group_id "$PG_ID"
392+
else
393+
echo "Verification failed, not starting flow"
394+
exit 1
395+
fi
360396
361397
Cross-References
362398
================

docs/history.rst

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,51 @@
22
History
33
=======
44

5+
1.4.0 (2026-01-15)
6+
-------------------
7+
8+
| CLI exit codes, bulletins descendants, batch parameter updates, and throughput stats
9+
10+
**CLI Improvements**
11+
12+
- **Standardized error exit codes**: CLI now exits with code 1 when result contains ``error`` or ``errors`` key, enabling scripts to reliably detect operational failures (not just exceptions)
13+
- **Error field convention**: CI functions now include ``error`` key for failures - documented convention for custom CI functions
14+
15+
**Bulletins Module**
16+
17+
- **get_bulletin_board()**: New ``descendants=True`` parameter to include bulletins from child process groups (default behavior matches processor/controller scope)
18+
19+
**Canvas Module**
20+
21+
- **list_all_controllers()**: Added ``greedy`` and ``identifier_type`` parameters for flexible process group resolution - now accepts ID, name, or ProcessGroupEntity
22+
23+
**CI Module Enhancements**
24+
25+
- **get_status()**: Added throughput stats (``flowfiles_in/out``, ``bytes_in/out``, ``bytes_read/written``) and fixed bulletin collection to use descendants
26+
- **configure_inherited_params()**: Batched parameter updates to reduce cycle overhead - all parameters per context now updated in single API call instead of one-by-one
27+
- **cleanup()**: Changed ``message`` key to ``error`` for failure responses (CLI exit code support)
28+
- **verify_config()**: Added ``error`` key with failed component names when verification fails
29+
30+
**Parameters Module**
31+
32+
- **prepare_parameter()**: Clarified value semantics - omitting ``value`` preserves existing, ``value=None`` explicitly unsets, ``value=""`` sets empty string
33+
- **list_orphaned_contexts()**: New function to find parameter contexts not bound to any process groups
34+
- **rename_parameter_context()**: New function to rename a parameter context
35+
36+
**Layout Module**
37+
38+
- **PORT_QUEUE_BOX_WIDTH**: New constant (240px) documenting port-to-port connection queue box width
39+
40+
**Bug Fixes**
41+
42+
- Fixed parameter value removal to correctly handle ``value=None`` vs omitted value
43+
- Fixed bulletin board to include controller service bulletins via group_id regex pattern
44+
45+
**Documentation**
46+
47+
- CLI error handling guide with exit code convention
48+
- CI error field convention for function authors
49+
550
1.3.0 (2026-01-08)
651
-------------------
752

nipyapi/bulletins.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def get_bulletins():
6767
return nipyapi.nifi.FlowApi().get_bulletins()
6868

6969

70-
def get_bulletin_board(pg_id=None, source_name=None, message=None, limit=None):
70+
def get_bulletin_board(pg_id=None, source_name=None, message=None, limit=None, descendants=True):
7171
"""
7272
Retrieve bulletins from the bulletin board with optional filtering.
7373
@@ -80,6 +80,9 @@ def get_bulletin_board(pg_id=None, source_name=None, message=None, limit=None):
8080
source_name (str, optional): Filter by source component name (regex).
8181
message (str, optional): Filter by message content (regex).
8282
limit (int, optional): Maximum number of bulletins to return.
83+
descendants (bool): Include bulletins from child process groups (default True).
84+
Only applies when pg_id is specified. If False, only returns bulletins
85+
from components directly in the specified process group.
8386
8487
Returns:
8588
list[BulletinDTO]: List of bulletin objects with direct field access.
@@ -101,15 +104,27 @@ def get_bulletin_board(pg_id=None, source_name=None, message=None, limit=None):
101104
102105
Example::
103106
107+
>>> # Get bulletins from a PG and all its children (default)
104108
>>> bulletins = nipyapi.bulletins.get_bulletin_board(pg_id="abc-123")
105109
>>> for b in bulletins:
106110
... print(f"{b.source_name}: {b.message}")
107-
... if b.stack_trace:
108-
... print(f" Stack: {b.stack_trace[:100]}...")
111+
112+
>>> # Get bulletins only from components directly in the PG
113+
>>> bulletins = nipyapi.bulletins.get_bulletin_board(pg_id="abc-123", descendants=False)
109114
"""
110115
kwargs = {}
116+
117+
# Build group_id filter - use regex to include descendants if requested
111118
if pg_id is not None:
112-
kwargs["group_id"] = pg_id
119+
if descendants:
120+
# Get all child PG IDs recursively
121+
all_pgs = nipyapi.canvas.list_all_process_groups(pg_id=pg_id)
122+
pg_ids = [pg_id] + [pg.id for pg in all_pgs]
123+
# Build regex pattern: "id1|id2|id3..."
124+
kwargs["group_id"] = "|".join(pg_ids)
125+
else:
126+
kwargs["group_id"] = pg_id
127+
113128
if source_name is not None:
114129
kwargs["source_name"] = source_name
115130
if message is not None:

nipyapi/canvas.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1929,22 +1929,43 @@ def create_controller(parent_pg, controller, name=None):
19291929
)
19301930

19311931

1932-
def list_all_controllers(pg_id="root", descendants=True, include_reporting_tasks=False):
1932+
def list_all_controllers(
1933+
pg_id="root",
1934+
descendants=True,
1935+
include_reporting_tasks=False,
1936+
greedy=True,
1937+
identifier_type="auto",
1938+
):
19331939
"""
19341940
Lists all controllers under a given Process Group, defaults to Root.
19351941
Optionally recurses all child Process Groups as well.
19361942
19371943
Args:
1938-
pg_id (str): String of the ID of the Process Group to list from
1944+
pg_id (str): The Process Group to list from, as a UUID string,
1945+
process group name, or ProcessGroupEntity object. Defaults to root.
19391946
descendants (bool): True to recurse child PGs, False to not
19401947
include_reporting_tasks (bool): True to include Reporting Tasks, False to not
1948+
greedy (bool): For name lookup, True for partial match, False for exact.
1949+
identifier_type (str): How to interpret string identifier:
1950+
"auto" (default) detects UUID vs name, "id" or "name" to force.
19411951
19421952
Returns:
19431953
None, ControllerServiceEntity, or list(ControllerServiceEntity)
19441954
19451955
"""
1946-
assert isinstance(pg_id, str)
19471956
assert isinstance(descendants, bool)
1957+
assert pg_id == "root" or isinstance(pg_id, (str, nipyapi.nifi.ProcessGroupEntity))
1958+
# Resolve pg_id to actual ID (supports name lookup)
1959+
if pg_id != "root":
1960+
process_group = nipyapi.utils.resolve_entity(
1961+
pg_id,
1962+
get_process_group,
1963+
nipyapi.nifi.ProcessGroupEntity,
1964+
strict=True,
1965+
greedy=greedy,
1966+
identifier_type=identifier_type,
1967+
)
1968+
pg_id = process_group.id
19481969
handle = nipyapi.nifi.FlowApi()
19491970
# Testing shows that descendant doesn't work on NiFi-1.1.2
19501971
# Or 1.2.0, despite the descendants option being available
@@ -2348,15 +2369,19 @@ def get_controller_service_docs(controller):
23482369
)
23492370

23502371

2351-
def list_all_by_kind(kind, pg_id="root", descendants=True):
2372+
def list_all_by_kind(kind, pg_id="root", descendants=True, greedy=True, identifier_type="auto"):
23522373
"""
23532374
Retrieves a list of all instances of a supported object type
23542375
23552376
Args:
23562377
kind (str): one of input_ports, output_ports, funnels, controllers,
23572378
connections, remote_process_groups
2358-
pg_id (str): optional, ID of the Process Group to use as search base
2379+
pg_id: The Process Group to list from, as a UUID string,
2380+
process group name, or ProcessGroupEntity object. Defaults to root.
23592381
descendants (bool): optional, whether to collect child group info
2382+
greedy (bool): For name lookup, True for partial match, False for exact.
2383+
identifier_type (str): How to interpret string identifier:
2384+
"auto" (default) detects UUID vs name, "id" or "name" to force.
23602385
23612386
Returns:
23622387
list of the Entity type of the kind, or single instance, or None
@@ -2371,7 +2396,20 @@ def list_all_by_kind(kind, pg_id="root", descendants=True):
23712396
"remote_process_groups",
23722397
]
23732398
if kind == "controllers":
2374-
return list_all_controllers(pg_id, descendants)
2399+
return list_all_controllers(
2400+
pg_id, descendants, greedy=greedy, identifier_type=identifier_type
2401+
)
2402+
# Resolve pg_id to actual ID (supports name lookup)
2403+
if pg_id != "root":
2404+
process_group = nipyapi.utils.resolve_entity(
2405+
pg_id,
2406+
get_process_group,
2407+
nipyapi.nifi.ProcessGroupEntity,
2408+
strict=True,
2409+
greedy=greedy,
2410+
identifier_type=identifier_type,
2411+
)
2412+
pg_id = process_group.id
23752413
handle = nipyapi.nifi.ProcessGroupsApi()
23762414
call_function = getattr(handle, "get_" + kind)
23772415
out = []

nipyapi/ci/cleanup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def cleanup(
114114
"stopped": "false",
115115
"deleted": "false",
116116
"process_group_name": "",
117-
"message": "Process group not found",
117+
"error": "Process group not found",
118118
}
119119
raise
120120

@@ -123,7 +123,7 @@ def cleanup(
123123
"stopped": "false",
124124
"deleted": "false",
125125
"process_group_name": "",
126-
"message": "Process group not found",
126+
"error": "Process group not found",
127127
}
128128

129129
pg_name = process_group.component.name

nipyapi/ci/configure_inherited_params.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def configure_inherited_params(
152152
)
153153
if ctx_id not in updates_by_context:
154154
updates_by_context[ctx_id] = []
155-
updates_by_context[ctx_id].append((param_name, param_value, False))
155+
updates_by_context[ctx_id].append((param_name, param_value, False, None))
156156
else:
157157
errors.append(
158158
f"Parameter '{param_name}' not found in any context. "
@@ -183,20 +183,38 @@ def configure_inherited_params(
183183
if owner["context_id"] not in updates_by_context:
184184
updates_by_context[owner["context_id"]] = []
185185
updates_by_context[owner["context_id"]].append(
186-
(param_name, param_value, owner["sensitive"])
186+
(param_name, param_value, owner["sensitive"], owner["description"])
187187
)
188188

189-
# Execute if not dry run
189+
# Execute if not dry run - batch all parameters per context to avoid
190+
# multiple disable/enable cycles. Each update_parameter_context call
191+
# triggers a full cycle (stop processors -> disable controllers -> update
192+
# -> re-enable -> restart), so we must batch all params for a context.
190193
if not dry_run and not errors:
191194
for target_ctx_id, param_updates in updates_by_context.items():
192-
for param_name, param_value, _sensitive in param_updates:
193-
log.info("Updating %s in context %s", param_name, target_ctx_id)
194-
nipyapi.parameters.update_parameter_in_context(
195-
context_id=target_ctx_id,
196-
param_name=param_name,
197-
value=param_value,
198-
create_if_missing=allow_override,
195+
log.info(
196+
"Updating %d parameter(s) in context %s",
197+
len(param_updates),
198+
target_ctx_id,
199+
)
200+
201+
# Get current context for revision info
202+
ctx = nipyapi.parameters.get_parameter_context(target_ctx_id, identifier_type="id")
203+
if ctx is None:
204+
errors.append(f"Parameter context not found: {target_ctx_id}")
205+
continue
206+
207+
# Build parameter entities from planning data
208+
param_entities = [
209+
nipyapi.parameters.prepare_parameter(
210+
name=name, value=value, description=desc, sensitive=sens
199211
)
212+
for name, value, sens, desc in param_updates
213+
]
214+
215+
# Update all parameters in one batch
216+
ctx.component.parameters = param_entities
217+
nipyapi.parameters.update_parameter_context(ctx)
200218

201219
# Build result
202220
result = {

0 commit comments

Comments
 (0)