Skip to content

feat(samples): support project, organism and pubmed on sample imports - #20

Merged
mhusbynflow merged 7 commits into
masterfrom
mhusbynflow/import-samples-project-organism
Aug 6, 2026
Merged

feat(samples): support project, organism and pubmed on sample imports#20
mhusbynflow merged 7 commits into
masterfrom
mhusbynflow/import-samples-project-organism

Conversation

@mhusbynflow

@mhusbynflow mhusbynflow commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

What changed

The v2 sample-import endpoint (POST /v2/sample-imports) accepts per-accession organism, project, and pubmed. organism was already wired up; this PR adds project and pubmed through both the library and the CLI.

Verified against the backend (flow-api samples/schemas.py::SampleImportRequest): it accepts organism: OrganismId | None, project: ProjectId | None, and pubmed: PubMedId | None (a bare, digits-only PubMed number). All three are validated server-side.

  • LibrarySampleImportSpec gains project_id (sent on the wire as project) and pubmed (sent unchanged). Both are optional and omitted from the payload when empty, mirroring how organism_id is handled. project_id is typed str to match the rest of the client (Project.id, upload_sample); a numeric-string id coerces to the backend's integer ProjectId server-side.
  • CLI — the accession sheet gains optional project and pubmed columns, parsed per row and forwarded via to_spec. --sheet help, module docstrings, and source/cli.rst (incl. the worked example) updated.
  • Version — bumped 0.10.00.11.0.

Verification

  • TDD red→green at each layer: library (spec → wire payload), CLI accession-sheet parser, and the end-to-end samples import --sheet seam through run_cli (both fields reach the request; blank cells are omitted).
  • Full unit suite: 393 passed.

Backwards-compatibility note (for release notes)

project and pubmed are now reserved accession-sheet columns. An existing sheet with a metadata column literally named project or pubmed will have that value reinterpreted as the project/publication assignment instead of forwarded as metadata. Unlikely in practice, but worth calling out.

Deferred to follow-ups

  • Exposing pubmed on upload_sample / the reads sheet (imports and uploads are different endpoints; upload's GraphQL mutation does accept $pubmed).
  • A repo-wide ProjectId/PubMedId NewType typing sweep (touches pre-existing bare-str project ids well outside this PR).

🤖 Generated with Claude Code

mhusbynflow and others added 2 commits August 6, 2026 11:38
The sample-import backend now accepts a project (alongside the
already-supported organism), so surface it through both the library
and the CLI.

SampleImportSpec gains an optional project_id, sent on the wire as
`project` and omitted when empty — mirroring how organism_id is
handled. The CLI accession sheet gains a matching optional `project`
column, forwarded per row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mhusbynflow

Copy link
Copy Markdown
Collaborator Author

@claude please review this PR

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Claude finished @mhusbynflow's task in 3m 19s —— View job


Review: feat(samples): support project on sample imports

  • Read the PR diff (git diff origin/master...HEAD)
  • Read flowbio/v2/samples.py — spec, payload builder, surrounding project handling
  • Read flowbio/cli/_accession_sheet.py in full
  • Cross-check against the reads-based sheet (flowbio/cli/_sheet.py, batch-template)
  • Check docs (source/cli.rst, source/v2/samples.rst) and CLAUDE.md conventions
  • Post review

Clean, well-scoped change — it mirrors the existing organism_id handling exactly, at every layer, and the docs/tests move with it. Nothing blocking. Notes below are all OPTIONAL.

What's good

  • _import_spec_fields (flowbio/v2/samples.py:534-546) keeps rename + omit symmetric with organism, and the docstring's "a field added to SampleImportSpec is sent even when unset unless it's also added to optional" contract was updated rather than left to drift.
  • project as a reserved column matches _sheet.py:20, so the two sheets stay conceptually aligned (the accession sheet is now reserved-column-identical to the reads sheet minus reads1/reads2).
  • Test coverage hits the cases that matter: parsed value, absent value, empty-string omission from the wire payload (exact-dict assertion at tests/unit/v2/test_samples.py:1163), and — implicitly but genuinely — that project is not swept into metadata, since test_carries_name_organism_project_and_metadata sets project and still asserts metadata == {"cell_type": "Neuron"}.
  • Removing the stale "the import API has no project field" note in both the module docstring and source/cli.rst is the right cleanup, and source/cli.rst:383 correctly narrows the batch-template rationale to "no reads files".

Optional findings

1. _import_command docstring is now the only place that omits projectflowbio/cli/_samples.py:622 still reads "the accession format, sample type, organism, and metadata rules are all validated server-side". The equivalent sentence in _accession_sheet.py:9 was updated in this PR; this one wasn't. One-word fix, keeps the two from drifting. Fix this →

2. Not strictly additive for existing sheets. The PR body calls 0.11.0 "additive, backwards-compatible" — true for the library API, but adding project to RESERVED_COLUMNS (_accession_sheet.py:30) silently changes meaning for an existing sheet that happens to have a metadata column literally named project: it used to be forwarded as a metadata key, and now it becomes the project assignment. Very unlikely in practice (and the reads sheet has always reserved it), but worth a line in release notes rather than being discovered in the field.

3. ProjectId domain type (repo typing convention). CLAUDE.md asks for named types over bare str for primitive identifiers, and this module already does that for SampleTypeId (samples.py:48) and SampleImportJobId (samples.py:144). project_id: str | None is consistent with the existing bare-str project ids (Project.id at samples.py:106, upload_sample(project_id=...) at samples.py:255, SheetRow.project), so matching them here is defensible — but a ProjectId = NewType("ProjectId", str) applied at creation (Project.id) and threaded through both upload_sample and SampleImportSpec would be the convention-following version. Reasonable to defer as its own change; flagging so the decision is explicit. Fix this →

4. Design question, not a defect: no --project flag for samples import. The documented rationale for column-only sample_type (a mixed-type sheet needs no special handling) doesn't transfer as cleanly to project: the common batch case is "import these 50 accessions into one project", which now means repeating the same id down 50 rows — and samples upload does have --project ID (source/cli.rst:141). If sheet-only is deliberate, fine; just confirming it's a choice rather than an omission.

Verification

I read the code rather than executing it — running pytest was blocked by tool permissions in this environment, so I can't independently confirm the 392-passing figure. The Tests workflow (.github/workflows/tests.yaml, py3.11–3.13) covers it. No linter is configured in CI, and the two slightly-long lines this PR adds are well within what the repo already tolerates elsewhere, so no finding there.
· branch mhusbynflow/import-samples-project-organism

Comment thread tests/unit/cli/test_accession_sheet.py Outdated
The sample-import endpoint accepts a per-accession pubmed id
(a bare PubMed number) alongside organism and project, so surface it
through the library and the CLI too.

SampleImportSpec gains an optional pubmed field, sent on the wire
unchanged and omitted when empty. The CLI accession sheet gains a
matching optional pubmed column, forwarded per row. Format is
validated server-side, like the other fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mhusbynflow

Copy link
Copy Markdown
Collaborator Author

@claude please re-review — added pubmed support to the sample-import flow (SampleImportSpec + accession sheet), mirroring project/organism.

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Claude finished @mhusbynflow's task in 2m 11s —— View job


Re-review: pubmed on sample imports (commit 22c690f)

  • Read the updated diff (git diff origin/master...HEAD)
  • Review the pubmed commit across lib (flowbio/v2/samples.py) + CLI (_accession_sheet.py, _samples.py)
  • Check whether the prior BLOCKING test-gap finding was addressed
  • Cross-check reads sheet (_sheet.py), batch-template, and docs consistency
  • Post review

pubmed mirrors project/organism faithfully at every layer — spec field, optional tuple (no rename needed, unlike the two _id fields), reserved column, _build_row, to_spec, --sheet help, source/cli.rst prose and worked example. Docstrings moved with the code, including the "sent even when unset unless it's also added to optional" contract. The :meth:SampleResource.get_owned_projects`` cross-reference added in the project docstring resolves (flowbio/v2/samples.py:449).

One blocking item, unchanged from my last review and now larger in scope.

1. BLOCKING — the end-to-end CLI seam still doesn't see project or pubmed

tests/unit/cli/test_samples.py is untouched by both commits:

  • IMPORT_HEADERS (tests/unit/cli/test_samples.py:929) is still ["accession", "name", "organism", "sample_type", "cell_type", "source", "source__annotation"], so no test in TestSamplesImport ever writes a sheet containing either new column.
  • test_sends_name_organism_and_metadata_in_payload (tests/unit/cli/test_samples.py:1053) still asserts an exact payload with neither project nor pubmed.

Both new fields are covered at the two unit seams — sheet row → SampleImportSpec (tests/unit/cli/test_accession_sheet.py:426) and SampleImportSpec → wire payload (tests/unit/v2/test_samples.py:1128, plus the empty-string omission assertion at :1160 which correctly includes pubmed=""). But the join between them is untested: this is the only test in the repo that drives flowbio samples import --sheet through run_cli to a mocked request, which is exactly the "exercise it through the public CLI" style CLAUDE.md asks for. It's what would catch a column reserved in the parser but dropped on the way to the request, or one leaking into metadata instead of onto the spec.

The gap has doubled rather than closed since the last round, so it's worth doing once for both fields:

  • add "project" and "pubmed" to IMPORT_HEADERS;
  • extend that payload test's record with project="proj_1", pubmed="12345678" and its expected imports[0] with the matching keys;
  • add a case where both cells are blank and assert the payload has neither key — the common shape for optional columns, and the one that would regress silently.

Fix this →

2. _import_command docstring — still stale, now two fields behind

flowbio/cli/_samples.py:618 reads "the accession format, sample type, organism, and metadata rules are all validated server-side". The equivalent sentence in _accession_sheet.py:9 was updated for both project and pubmed; this one was updated for neither. Same one-line fix as last round, now covering both. Fix this →

3. The two sheets are no longer reserved-column-aligned (optional)

Last round I noted that adding project made the accession sheet reserved-column-identical to the reads sheet minus reads1/reads2. pubmed breaks that: _sheet.py:20 is ("name", "reads1", "reads2", "project", "organism"), and batch-template's column list (_samples.py:404 and around) has no pubmed either — even though the underlying upload mutation does accept one (flowbio/mutations.py:23, $pubmed: String) and SampleResource.upload_sample (samples.py:252) doesn't expose it. Not a defect in this PR — imports and uploads are genuinely different endpoints — but the asymmetry is now user-visible (pubmed works in one sheet, silently becomes a metadata key in the other). Worth either a follow-up issue to add pubmed to upload_sample/_sheet.py, or a deliberate "imports only" note.

4. Backwards-compat caveat now applies to two column names (optional)

Same shape as the project note: an existing accession sheet with a metadata column literally named pubmed used to be forwarded as a metadata key and now becomes the publication assignment. project at least had the reads-sheet precedent; pubmed is newly reserved in both senses. Still very unlikely, but the 0.11.0 release notes are the cheap place to say it.

5. ProjectId / PubMedId named types (optional, carried over)

CLAUDE.md prefers NewType over bare str for primitive identifiers, and this module already does it for SampleTypeId and SampleImportJobId. pubmed: str | None extends the bare-str set rather than shrinking it. As before: consistent with the surrounding code, defensible to defer, flagging so it stays an explicit decision rather than drift. Note str (not int) is right regardless — it matches the CSV cell and the String GraphQL type.

Non-findings

  • Field ordering in the wire payload shifts (organism/project get popped and re-added last), but that's JSON object ordering — no behavioural or test impact.
  • tests/unit/cli/test_accession_sheet.py:10 (135 chars) and _accession_sheet.py:9 (95) are long, but the repo has no lint config and several longer lines already (flowbio/v2/exceptions.py:41). Not raising it.
  • source/cli.rst:452-454 — checked the worked example's comma counts; both rows have 6 fields matching the 6 headers.

Verification

Code read only — python -m pytest tests/unit was blocked by tool permissions in this environment (same as last run), so I can't independently confirm a green suite. The Tests workflow (py3.11–3.13) covers it. Add Bash(python -m pytest*) to --allowedTools if you'd like me to run it next time.
· branch mhusbynflow/import-samples-project-organism

Comment thread flowbio/v2/samples.py Outdated
Comment thread flowbio/v2/samples.py
The end-to-end `samples import --sheet` test is the only one that drives
the accession sheet through the CLI to a mocked request, so it's what
catches a column being reserved in the parser but dropped before the
request (or leaking into metadata). It didn't exercise the new project
or pubmed columns.

Add both to IMPORT_HEADERS, assert they reach the payload, and add a
case proving blank cells are omitted from it. Also bring the
_import_command docstring's server-validated field list in line with
the module docstring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mhusbynflow

Copy link
Copy Markdown
Collaborator Author

@claude please re-review — addressed the blocking CLI-seam test gap (project + pubmed now covered end-to-end through samples import --sheet, incl. a blank-cell omission case) and the _import_command docstring. Deferring the upload_sample/pubmed asymmetry and the release-note caveat to follow-ups.

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

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


Re-review: CLI-seam coverage (commit 5f85cf5)

  • Read the new commit's diff
  • Verify the blocking CLI-seam gap is closed (project + pubmed + blank-cell case)
  • Verify the _import_command docstring fix
  • Re-check the carried-over pubmed question
  • Post review

The blocking finding is resolved. Both new columns are now exercised through the only end-to-end samples import --sheet path in the repo, in both directions. Nothing blocking remains. One cosmetic nit and one description update below.

Blocking item — closed ✅

Verified against the three things I asked for:

  • IMPORT_HEADERS (tests/unit/cli/test_samples.py:929) now carries project and pubmed, so every sheet written by _write_import_sheet has the columns present — which is what makes the omission case meaningful rather than vacuous.
  • test_sends_optional_columns_in_payload (:1054) sets project="proj_1", pubmed="12345678" on the record and asserts an exact imports[0] containing both — and, because it still asserts metadata == {"cell_type": "Neuron"}, it simultaneously proves neither field leaks into metadata. That's the parser-reserves-it-but-drops-it failure mode covered from both sides.
  • test_blank_optional_columns_are_omitted_from_payload (:1082) asserts the exact payload is {"accession": "ERR1", "sample_type": "rna_seq"} — no project, no pubmed, and no empty name/organism/metadata either. The exact-dict form is the right call; a not in assertion would have missed a null sneaking through.

The rename from test_sends_name_organism_and_metadata_in_payload to test_sends_optional_columns_in_payload is also correct — the name now describes the invariant rather than an enumeration that would go stale on the next optional field.

Trace-checked the seam by hand end to end: RESERVED_COLUMNS (_accession_sheet.py:30) → _build_row (:254-255) → AccessionSheetRow.to_spec (:64-65) → _import_spec_fields rename+omit (flowbio/v2/samples.py:545-548). Consistent at every hop, and _cell (:231-234) normalises ""None so the blank-cell path hits the same optional filter as an unset field.

1. OPTIONAL — the docstring re-wrap left one ragged line

flowbio/cli/_samples.py:624 is 108 characters where the rest of its own paragraph wraps at ~72:

    organism, project, pubmed, and metadata rules are all validated
    server-side, so a malformed sheet surfaces as a normal
    :class:`FlowApiError` rather than a local pre-flight rejection. This command does not wait for the job to
    finish — poll it yourself with ``samples import-status``.

Content is right — this was the fix I asked for and it now matches _accession_sheet.py:9. It's purely the reflow: the two words added pushed the sentence boundary and the paragraph wasn't re-wrapped after. No linter enforces this (nothing in CI, and the repo has longer lines at _samples.py:402-404), so it's cosmetic — but it's visibly ragged against its immediate neighbours in a way those aren't. Fix this →

2. The PR description is now two commits stale

Not a code finding, but it's what a reviewer or release-notes author reads first. The body still says "the sample-import backend now accepts a project" and "only project was missing", and its Verification section smoke-tests only project reaching the wire. pubmed ships in the same PR with equal footprint.

That also leaves my earlier inline question (here) unanswered: nothing in this repo evidences that POST /v2/sample-imports accepts pubmed — the only other occurrence is a GraphQL sample field in flowbio/queries.py, and the unit tests assert against a mocked payload, so they pass either way. I can't verify a backend contract from the checkout; you can. I'm not treating it as blocking — you've added it deliberately and described it as backend-supported — but a sentence in the body saying the endpoint accepts it turns that from my assumption into a recorded fact. Same place to note the RESERVED_COLUMNS caveat (a sheet with a metadata column literally named project/pubmed changes meaning), which you've flagged for release notes.

Deferred items — no objection

upload_sample/_sheet.py pubmed asymmetry (my #3 last round) and the ProjectId/PubMedId NewType question (#5) are both reasonable follow-ups: the first spans two genuinely different endpoints, and the second is a repo-wide typing sweep that touches pre-existing bare-str project ids well outside this PR. Deferring keeps this change single-purpose. Worth an issue for each so they don't evaporate.

Verification

Code read and traced only. python -m pytest tests/unit was blocked by tool permissions again this run, so I still can't independently confirm the suite is green — the Tests workflow (py3.11–3.13) is the source of truth for that. Add Bash(python -m pytest*) to --allowedTools if you'd like me to run it next time.
· branch mhusbynflow/import-samples-project-organism

Comment thread flowbio/v2/samples.py Outdated
Comment thread flowbio/cli/_accession_sheet.py Outdated
Comment thread flowbio/cli/_samples.py Outdated
@mhusbynflow mhusbynflow changed the title feat(samples): support project on sample imports feat(samples): support project, organism and pubmed on sample imports Aug 6, 2026
Adding project and pubmed to the server-validated field list pushed the
sentence boundary and left one line ragged against its neighbours.
Re-wrap only; no wording change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread flowbio/v2/samples.py Outdated
Comment thread flowbio/v2/samples.py Outdated
mhusbynflow and others added 2 commits August 6, 2026 15:31
…edId

Two review-driven refinements to the sample-import payload:

Invert the payload filter from an optional-field denylist to a
required-field allowlist. `optional` had to grow in lockstep with every
new field, and the docstring had to warn about the failure mode. Listing
the two always-sent fields instead makes omit-when-empty the default, so
a field added to SampleImportSpec is dropped-when-empty for free.
Semantics are unchanged (required and optional together covered every
field).

Add a PubMedId named type (str, documented as a bare integer, validated
server-side) alongside SampleTypeId, and apply it to
SampleImportSpec.pubmed and the accession-sheet row — matching how
SampleTypeId is already wrapped where the value is created. Exported
from flowbio.v2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The import section's "validated server-side" sentence listed only
accession, sample type, and metadata — now that organism, project, and
pubmed all go through the same server-side validation, name them too, in
step with the module and command docstrings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread flowbio/v2/samples.py
@mhusbynflow
mhusbynflow merged commit c570a17 into master Aug 6, 2026
6 checks passed
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