Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
270657d
feat: seperate broker contract endpoints and add tests
emilylm Mar 31, 2026
ee5c69d
feat: handle accessino pre-reqs for broker endpoints
emilylm Mar 31, 2026
baa0d1d
feat: handle accession pre-reqs for broker endpoints
emilylm Apr 13, 2026
e62cc53
feat: update broker
emilylm Apr 1, 2026
33915eb
feat: update broker
emilylm Apr 1, 2026
a7450e0
feat: docs
emilylm Apr 1, 2026
88deee4
feat: set organism_part to WHOLE ORGANISM for specimen samples
emilylm Apr 13, 2026
609b704
feat: add batch broker endpoint to request multiple objects by id
emilylm Apr 13, 2026
14e5c03
feat: normalise fk references for submissioon objects, denormalise ac…
emilylm Apr 13, 2026
5c08f6b
style: linting
emilylm Apr 13, 2026
c2154ca
feat: add project_id fk to sample_submission table
emilylm Apr 13, 2026
b35c2b1
feat: add project_id fk to sample_submission table
emilylm Apr 13, 2026
efae4c4
feat: update accession registry references
emilylm Apr 13, 2026
d988950
feat: return meaningful error when reporting entities whose accession…
emilylm Apr 13, 2026
5329976
fix: some incorrect fields and move default value from db schema to code
emilylm Apr 13, 2026
affb397
feat: only require attempt_id as path param in /broker/report endpoint
emilylm Apr 13, 2026
0ac67c5
feat: consolidate references to project and study accessions, which a…
emilylm Apr 15, 2026
b6aa696
feat: remove validation_hints from broker endpoint, broker can decide…
emilylm Apr 15, 2026
0b4dd81
feat: remove study_accession references
emilylm Apr 15, 2026
2f9ba27
feat: update submission table references
emilylm Apr 15, 2026
56bd98a
feat: update references from submission tables
emilylm Apr 15, 2026
83eca1b
feat: return scientific_name
emilylm Apr 22, 2026
1c017a8
feat: create new submission object when submission fails to ENA
emilylm Apr 22, 2026
08cd108
refactor: consolidate migrations from this branch into one migration,…
emilylm Apr 22, 2026
a642183
style: linting
emilylm Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions BROKER_PREREQUISITES_ENHANCEMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Broker Prerequisites Enhancement

## Problem
The broker API was returning null values for `prerequisites`, `validation_hints`, and `file_metadata` fields because:
1. Missing accession data in submission payloads
2. No way to distinguish between "not required" vs "required but not yet submitted"

## Solution
Enhanced the broker contract to include **existing** accessions (resolved from `accession_registry`), allowing clients to decide whether dependency state is complete enough to submit.

### Key Architecture Understanding

**Crucial clarification**: Accessions are stored in different places:
- **Existing accessions**: Stored in database row fields (`project_accession`, `sample_accession`, etc.) and `accession_registry` table
- **prepared_payload**: Contains data for ENA submission, NOT existing accessions
- **Required accessions**: Specified in payload using `expected_*_accession` fields for dependencies not yet submitted

### Changes Made

#### 1. BrokerPrerequisites Schema
```python
class BrokerPrerequisites(BaseModel):
# Existing accessions (from database row fields)
project_accession: Optional[str] = None
sample_accession: Optional[str] = None
experiment_accession: Optional[str] = None
run_accession: Optional[str] = None
study_accession: Optional[str] = None
analysis_accession: Optional[str] = None
```

#### 2. Enhanced Prerequisite Extraction Logic
- **Existing accessions**: Looked up from `accession_registry` via FK relationships on submission tables

#### 3. Updated Test Cases
- Tests now verify both existing and required accessions
- Demonstrates mixed states where some dependencies exist and others don't

### Usage Examples

#### Sample Submission with Existing Project Accession
**Database state**: `accession_registry` contains a project accession for the sample's `project_id`
```json
{
"alias": "sample-1",
"requires_project_accession": true
}
```
Result:
```json
{
"prerequisites": {
"project_accession": "PRJ123456" // βœ… Resolved from accession_registry
}
}
```

#### Experiment with Missing Study Accession
**Database state**: `accession_registry` does not contain a project accession for the experiment's `project_id`
```json
{
"alias": "experiment-1",
"requires_study_accession": true,
"expected_study_accession": "PRJ123456" // Required but not submitted
}
```
Result:
```json
{
"prerequisites": {
"sample_accession": "SAMEA123456", // βœ… Resolved from accession_registry
"study_accession": null // ❌ Missing from registry
}
}
```

#### Run with Missing Experiment Accession
**Database state**: `accession_registry` does not contain an experiment accession for the run's `experiment_id`
```json
{
"alias": "run-1",
"file_name": "reads.fastq.gz",
"file_format": "fastq",
"expected_experiment_accession": "ERX123456" // Required but not submitted
}
```
Result:
```json
{
"prerequisites": {
"experiment_accession": null // ❌ Missing from registry
},
"files": [
{"filename": "reads.fastq.gz", "filetype": "fastq"}
]
}
```

### Data Flow Summary

1. **prepared_payload**: Contains ENA submission data (metadata, files, etc.)
2. **Database row fields**: Store actual accessions once submitted (`*_accession` fields)
3. **Broker response**: Shows resolved accessions (or `null`), and clients decide completeness

### Client Benefits
1. **Clear dependency visibility**: See what exists vs what is missing
2. **Single source of truth**: Accessions resolved from `accession_registry`

### Payload Fields for Required Accessions
- `expected_*_accession`: Placeholder for the accession that will be assigned
- `requires_*_accession`: Boolean flag indicating the dependency is required
- `requires_project_accession`: For samples
- `requires_study_accession`: For experiments (maps to `required_project_accession` in broker prerequisites)

This enhancement maintains backward compatibility while providing much richer dependency information to broker clients, with the correct understanding of where different types of data are stored.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ A dedicated broker workflow enables integration with external submission pipelin
- Submission workflow endpoints (sample-submissions, experiment-submissions, read-submissions)
- Broker endpoints to support external submission pipelines:
- Claim drafts and obtain a lease: `/api/v1/broker/organisms/{organism_key}/claim`
- ENA broker contract endpoints: `/api/v1/broker/claims/ready`, `/api/v1/broker/claims/entity`, `/api/v1/broker/validation`, `/api/v1/broker/reports/{attempt_id}`
- Renew lease, finalise, and report results: `/api/v1/broker/attempts/{attempt_id}/...`
- Attempt listing and summaries for dashboard views
- Bulk import endpoints for organisms, samples, and experiments
Expand Down Expand Up @@ -236,6 +237,7 @@ pyproject.toml, uv.lock, docker-compose.yml, Dockerfile, schema.sql, scripts/, d
-d '{"samples": [], "experiments": [], "reads": [], "projects": []}'
```

For the flat ENA broker contract used by Canopy, see [docs/ena_broker_contract.md](docs/ena_broker_contract.md).
For a deeper overview of attempt leasing and statuses, see the `broker` endpoints in `app/api/v1/endpoints/broker.py` and the interactive docs.

## Bulk Import API
Expand Down
136 changes: 136 additions & 0 deletions alembic/versions/0006_consolidated_submission_schema_refactor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Consolidated submission schema refactor.

This migration consolidates the following changes:
- Add project_id to sample_submission (NOT NULL, FK to project)
- Add project_id to experiment (NOT NULL, FK to project)
- Drop project_id from read_submission (derivable via experiment)
- Drop sample_id and project_id from experiment_submission (derivable via experiment)

Note: No backfill logic included - database will be repopulated after merge.

Revision ID: 0006_consolidated_refactor
Revises: 0005_org_sci_name_nullable
Create Date: 2026-04-22 00:00:00.000000
"""

import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from alembic import op

# revision identifiers, used by Alembic.
revision = "0006_consolidated_refactor"
down_revision = "0005_org_sci_name_nullable"
branch_labels = None
depends_on = None


def upgrade():
# 1) Add sample_submission.project_id (NOT NULL, FK to project)
op.add_column(
"sample_submission",
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
)
op.create_foreign_key(
"fk_sample_submission_project_id",
"sample_submission",
"project",
["project_id"],
["id"],
)
op.create_index(
"idx_sample_submission_project_id",
"sample_submission",
["project_id"],
unique=False,
)

# 2) Add experiment.project_id (NOT NULL, FK to project with CASCADE)
op.add_column(
"experiment",
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
)
op.create_foreign_key(
"fk_experiment_project_id",
"experiment",
"project",
["project_id"],
["id"],
ondelete="CASCADE",
)
op.create_index(
"idx_experiment_project_id",
"experiment",
["project_id"],
unique=False,
)

# 3) Drop read_submission.project_id (derivable via experiment)
op.execute(
"ALTER TABLE read_submission DROP CONSTRAINT IF EXISTS read_submission_project_id_fkey"
)
op.execute(
"ALTER TABLE read_submission DROP CONSTRAINT IF EXISTS read_submission_project_id_fkey1"
)
op.execute("ALTER TABLE read_submission DROP COLUMN IF EXISTS project_id")

# 4) Drop experiment_submission.sample_id and project_id (derivable via experiment)
op.execute(
"ALTER TABLE experiment_submission DROP CONSTRAINT IF EXISTS experiment_submission_sample_id_fkey"
)
op.execute(
"ALTER TABLE experiment_submission DROP CONSTRAINT IF EXISTS experiment_submission_project_id_fkey"
)
op.execute("ALTER TABLE experiment_submission DROP COLUMN IF EXISTS sample_id")
op.execute("ALTER TABLE experiment_submission DROP COLUMN IF EXISTS project_id")


def downgrade():
# Reverse order of upgrade

# 4) Re-add experiment_submission.sample_id and project_id
op.add_column(
"experiment_submission",
sa.Column("sample_id", postgresql.UUID(as_uuid=True), nullable=True),
)
op.add_column(
"experiment_submission",
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=True),
)
op.create_foreign_key(
"experiment_submission_sample_id_fkey",
"experiment_submission",
"sample",
["sample_id"],
["id"],
)
op.create_foreign_key(
"experiment_submission_project_id_fkey",
"experiment_submission",
"project",
["project_id"],
["id"],
)

# 3) Re-add read_submission.project_id
op.add_column(
"read_submission",
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=True),
)
op.create_foreign_key(
"read_submission_project_id_fkey",
"read_submission",
"project",
["project_id"],
["id"],
)

# 2) Drop experiment.project_id
op.drop_index("idx_experiment_project_id", table_name="experiment")
op.drop_constraint("fk_experiment_project_id", "experiment", type_="foreignkey")
op.drop_column("experiment", "project_id")

# 1) Drop sample_submission.project_id
op.drop_index("idx_sample_submission_project_id", table_name="sample_submission")
op.drop_constraint("fk_sample_submission_project_id", "sample_submission", type_="foreignkey")
op.drop_column("sample_submission", "project_id")
Loading
Loading