refactor: extract a shared _dispatch helper for the unary senders - #43
Merged
Conversation
The 18 unary _send_* methods across the five service clients each wrote out the same three-tier error-handling block -- gRPC exception, business exceptionalResult, unexpected exception -- differing only in the stub method, the success oneof field, the result class, and the operation name. Collapse them into delegations to one _dispatch() on ServiceApiClientBase, so the shape is written once and #6's 10 new senders cost ~6 lines each instead of ~40. Two details the ticket's sketched signature did not cover, both preserved rather than dropped (see plan/tickets/14/plan.md, T3-T5): - Each sender logs a bespoke INFO message, and seven of them report a count read off the response. _dispatch takes optional request_log/success_log callables so every existing message survives verbatim; senders that logged a bare "Calling <op> API" pass nothing and get the identical default. No test asserts on log content, so a silent drop here would not have failed. - IngestionClient alone guarded e.code() against a bare RpcError, the shape its own test raises. That guard is now shared behavior, which adds the code to the other 17 senders' logs where it resolves and changes no returned message. Error-message text is byte-identical throughout -- about 40 assertions match on it -- so the existing suite covers the refactored path unedited: 406 passing before, 406 after. The new test module covers _dispatch's own branches, including the code() guard no per-client test reaches (415 total). The two server-streaming senders keep their hand-written bodies: they yield one result per streamed message, error results included, which is a different contract from returning a single result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011miYLGXSFNyPJWhBJr2wLK
There was a problem hiding this comment.
🟡 Changes recommended
A claimed “byte-identical” logging invariant is not fully preserved (business-error log prefix case differs) and there’s a docstring typo in a touched hunk.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Refactors the Python gRPC client library to centralize the repeated three-tier unary RPC error-handling logic into a single ServiceApiClientBase._dispatch() helper, reducing duplication across service clients while keeping the existing result/exception contract.
Changes:
- Added
ServiceApiClientBase._dispatch()to handle unary stub invocation, business errors (exceptionalResult), gRPC errors, and unexpected exceptions. - Updated all 18 unary
_send_*methods across clients to delegate to_dispatch()(streaming senders remain bespoke). - Added focused unit tests for
_dispatch()and documented the new pattern inCLAUDE.md, plus a ticket plan underplan/tickets/14/.
File summaries
| File | Description |
|---|---|
| tests/unit/test_service_api_client_base.py | Adds direct unit coverage for _dispatch() branches, including the code() guard and default/custom logging hooks. |
| src/dp_python_lib/client/service_api_client_base.py | Introduces _dispatch() helper to standardize unary RPC dispatch + error handling and logging. |
| src/dp_python_lib/client/sample_status_client.py | Refactors unary senders to delegate to _dispatch() with preserved request/success logs. |
| src/dp_python_lib/client/query_client.py | Refactors unary sender to delegate to _dispatch(). |
| src/dp_python_lib/client/pv_metadata_client.py | Refactors unary senders to delegate to _dispatch() with preserved request/success logs. |
| src/dp_python_lib/client/machine_config_client.py | Refactors unary senders to delegate to _dispatch() with preserved request/success logs. |
| src/dp_python_lib/client/ingestion_client.py | Refactors unary sender to delegate to _dispatch(). |
| plan/tickets/14/plan.md | Adds a point-in-time plan/record for the refactor’s intent and constraints. |
| CLAUDE.md | Documents the _dispatch() pattern and clarifies unary vs streaming sender expectations. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Follow-ups from the review of the _dispatch extraction: - Type result_cls with a new ApiResultFactory protocol rather than type[ApiResultT]. ApiResultBase.__init__ takes only (is_error, message) -- the 'response' keyword is added by each concrete subclass -- so the old annotation made _dispatch's own construction call unsound, and mypy flagged it. The protocol states the three-argument shape _dispatch actually depends on. It also tightens checking: a class with an incompatible constructor is now rejected, which type[ApiResultT] could not catch. The precise per-sender return type is preserved. - Keep the business-error warning's raw camelCase op_name, so all three error tiers name the operation the same way, and document the choice at the call site and in CLAUDE.md. The hand-written senders capitalized it on that one line while already using camelCase in their other two error logs; that inconsistency was not worth carrying forward. Log text only -- returned messages are unchanged. A new test pins the format, since nothing else in the suite asserts on log content. - Name the 11 success_log lambda parameters that never read the response '_response', the convention for a required-but-unused parameter. - Fix a pre-existing "RegisgerProviderRequest" docstring typo. 416 unit tests pass; ruff check, ruff format --check, and the cookbook snippet checker are clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013pw6sPhFVGV7zUNWHnP6cg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #14.
The 18 unary
_send_*methods across the five service clients each wrote out the same three-tier error-handling block — gRPC exception, businessexceptionalResult, unexpected exception — differing only in the stub method, the success oneof field, the result class, and the operation name. They now delegate to one_dispatch()onServiceApiClientBase.Net −282 lines of client code, and each of #6's 10 new senders costs ~6 lines instead of ~40. Plan:
plan/tickets/14/plan.md.Triage notes
The ticket's premise held, and its corrected count of 18 unary senders is right (20
_send_*total, minus the 2 server-streaming). Three things it did not cover, all preserved rather than dropped:Success/entry logs are not uniform (plan T3/T4). Every sender logs a bespoke INFO line, and seven report a count read off the response (
"QueryPvMetadata returned %d records","Successfully saved %d sample status(es)"). The ticket's sketched signature has nowhere to put them, so a literal extraction would have silently dropped all 18 — and since no test asserts on log content (plan T6), nothing would have flagged it._dispatchtakes optionalrequest_log/success_logcallables; senders that logged a bare"Calling <op> API"pass nothing and get the byte-identical default.IngestionClientalone guardede.code()(plan T5) against a bareRpcError— the shape its own test raises, on whichcode()genuinely does not resolve. That guard is now shared behavior: it adds the code to the other 17 senders' logs where it resolves, and changes no returned message.The two server-streaming senders stay hand-written. They yield one result per streamed message, error results included for the public
iter_*wrapper to convert — a different contract from returning a single result.Verification
assertIn), so the existing suite covers the refactored path unedited: 406 passing before, 406 after.mainsender-by-sender, by rendering each sender's output in all six scenarios: all 18 emit from the same logger, with exactly two deliberate differences and no others:(code: ...)where the code resolves (theIngestionClientguard, generalized);op_name(savePvMetadata API returned business error) instead of the capitalized form the hand-written senders used on that one line, while already using camelCase in their other two error logs. That inconsistency was dropped deliberately rather than preserved; a test now pins the format. Log text only — returned messages are unchanged.tests/unit/test_service_api_client_base.pycovers_dispatch's own branches, including thecode()guard no per-client test reaches and the business-error log format — 416 total.ruff check/ruff format --checkclean; cookbook snippet checker passes (78 snippets).CLAUDE.md documents the pattern so #6 and later work write senders in the collapsed form.
🤖 Generated with Claude Code
https://claude.ai/code/session_011miYLGXSFNyPJWhBJr2wLK