Skip to content

feat(cli): add samples annotation-template and upload-multiplexed commands#13

Merged
mhusbynflow merged 2 commits into
masterfrom
mhusbynflow/mhusbyn/flow/samples-py-review
Jun 9, 2026
Merged

feat(cli): add samples annotation-template and upload-multiplexed commands#13
mhusbynflow merged 2 commits into
masterfrom
mhusbynflow/mhusbyn/flow/samples-py-review

Conversation

@mhusbynflow

Copy link
Copy Markdown
Collaborator

What

Implements User Story 5 (Phase 5) of the flowbio CLI — the full multiplexed upload flow:

  • samples annotation-template — downloads the server-generated annotation sheet (.xlsx) for a sample type (default generic, sent as-is) via the existing client.samples.get_annotation_template, writing the bytes to -o/--output. Since the body is binary, -o is required whenever it would clash with the stream that owns stdout — an interactive terminal or --json — failing fast with exit 2 before any network call. With a piped stdout in human mode it streams the workbook to stdout.
  • samples upload-multiplexed — wraps client.samples.upload_multiplexed_data, reporting data_ids, annotation_id, and any warnings. Warnings are reported but the upload proceeds by default (matching the library default); --reject-warnings opts into rejecting them (exit 5).
  • Docs for both commands added to docs/cli.md.

Design notes (from review feedback)

  • The samples parser registration is split into one _configure_* helper per verb, each taking a public argparse.ArgumentParser, so register() stays a concise verb registry and no argparse private type (_SubParsersAction) is referenced.
  • --reject-warnings (off by default → ignore_warnings=True) is intentional: the spec default is "proceed despite warnings". A --ignore-warnings flag defaulting false would invert that documented default.

Verification

  • TDD throughout: failing tests written and confirmed red before each handler.
  • 234 passed across tests/unit (78 in tests/unit/cli).
  • New verbs confirmed in flowbio samples --help.

Out of scope

  • Phases 6–8 (US3 batch-template, US4 upload-batch, polish) remain unimplemented.

🤖 Generated with Claude Code

…mands

Implements User Story 5 (Phase 5) of the flowbio CLI: the full multiplexed
upload flow of downloading a server-generated annotation sheet, filling it in,
and submitting multiplexed reads alongside it.

- `samples annotation-template` wraps the existing
  `get_annotation_template`, writing the `.xlsx` bytes to `-o/--output`.
  Because the body is binary, `-o` is required whenever it would clash with the
  stream that owns stdout — an interactive terminal or `--json` — failing fast
  with a usage error before any network call. With a piped stdout in human mode
  it streams the workbook to stdout.
- `samples upload-multiplexed` wraps `upload_multiplexed_data`, reporting the
  data identifiers, annotation identifier, and any warnings. Warnings are
  reported but the upload proceeds by default (matching the library default);
  `--reject-warnings` opts into rejecting them.

The samples parser registration is split into one `_configure_*` helper per
verb, each taking a public `argparse.ArgumentParser`, keeping `register()` a
concise verb registry without leaking argparse's private subparsers type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mhusbynflow

Copy link
Copy Markdown
Collaborator Author

@claude please review this PR

@claude

claude Bot commented Jun 9, 2026

Copy link
Copy Markdown

Claude finished @mhusbynflow's task in 1m 58s —— View job


Review: samples annotation-template & upload-multiplexed

  • Read changed files and diff against origin/master
  • Review _samples.py handlers
  • Review tests
  • Review docs

Overall this is a clean, well-structured PR. The per-verb _configure_* split keeps register() readable, the fast-fail before any network call when stdout is contended is the right call, and the error-path test coverage is solid. A few things worth addressing before merge.


🟠 Unguarded output write can crash with a traceback

flowbio/cli/_samples.py:194

destination.write_bytes(template)

If --output points at a path whose parent directory doesn't exist (or isn't writable), write_bytes raises FileNotFoundError/OSError. _dispatch (_main.py:87-92) only catches CliUsageError and FlowApiError, so this escapes main() as an uncaught traceback and exits 1 — not the clean exit 2 the rest of the command uses for input problems. Note the upload path deliberately guards its inputs via existing_file(...); the output here isn't guarded symmetrically. A try/except OSError re-raised as CliUsageError would keep the contract consistent.

Fix this →

🟡 The streamed-to-stdout branch is untested (and unreachable by the test harness)

flowbio/cli/_samples.py:188-193

if destination is None:
    output.stdout.buffer.write(template)

The PR documents "with a piped stdout in human mode it streams the workbook to stdout", but there's no test for it. It also can't easily be tested with the current run_cli fixture: it captures stdout as an io.StringIO, which has no .buffer attribute — so this branch would AttributeError under the test harness, and any regression in it would go unnoticed. The two guarded cases (TTY, --json) are well covered; this third documented case has zero coverage. Worth either adding a fixture that exposes a binary .buffer and asserting the bytes land on stdout, or at minimum noting the gap.

🟢 Nits

  • -o help text is incomplete (_samples.py:116): says "Required when stdout is a terminal", but it's equally required under --json. The docs get this right; the inline help doesn't.
  • Warning advisory prints a raw repr (_samples.py:225): f"Annotation warnings: {upload.warnings}" renders as Annotation warnings: [{'row': 1, 'message': '...'}]. Acceptable, but a touch raw for a human-facing line.

Confirmed good

  • --reject-warnings defaulting off (→ ignore_warnings=True) correctly preserves the documented "proceed despite warnings" default — inverting it with a --ignore-warnings flag would have been the wrong call.
  • Exit-code mapping lines up: AnnotationValidationErrorBadRequestError → exit 5, server not-found → exit 4, the pre-network -o guard → exit 2.
  • Docs correctly distinguish the annotation .xlsx from the batch CSV and document the binary-to-stdout guard and exit codes.

I couldn't run the test suite here (sandbox blocked pytest), so I'm taking the stated 234 passed on trust; the test code itself reads correctly against the mocked routes.
· mhusbynflow/mhusbyn/flow/samples-py-review

…ering

Addresses review feedback on the Phase 5 commands:

- Make `-o/--output` always required for `samples annotation-template` and
  drop the stdout-streaming branch. The workbook is binary and the streamed
  path was both untestable (the test harness's StringIO has no `.buffer`) and
  prone to clashing with the JSON document on stdout. Requiring an output path
  is a strict superset of the previous "required when stdout is a terminal"
  rule; the contract and docs are updated to match.
- Guard the output write against `OSError` (e.g. a missing parent directory or
  an unwritable path), re-raising as a `CliUsageError` so a bad `--output`
  returns exit 2 instead of an uncaught traceback — symmetric with the
  input-side `existing_file` guard.
- Correct the `-o` help text (no longer claims it is only conditionally
  required).
- Render annotation warnings one per line via `_format_warning` instead of a
  raw list-of-dicts repr.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mhusbynflow mhusbynflow merged commit 58f7548 into master Jun 9, 2026
4 checks passed
@mhusbynflow mhusbynflow deleted the mhusbynflow/mhusbyn/flow/samples-py-review branch June 9, 2026 13:29
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.

1 participant