|
| 1 | +--- |
| 2 | +description: 'MaveDB API patterns — routers, authentication, authorization, endpoints' |
| 3 | +applyTo: 'src/mavedb/routers/**/*.py' |
| 4 | +--- |
| 5 | + |
| 6 | +# API Patterns for MaveDB |
| 7 | + |
| 8 | +## Router Structure |
| 9 | + |
| 10 | +All routers use: |
| 11 | +- `ROUTER_BASE_PREFIX = "/api/v1"` from `src/mavedb/routers/__init__.py` |
| 12 | +- `LoggedRoute` as the custom `route_class` for canonical request/response logging |
| 13 | +- Kebab-case URL paths: `/score-sets`, `/experiment-sets` |
| 14 | + |
| 15 | +```python |
| 16 | +router = APIRouter( |
| 17 | + prefix="/api/v1/score-sets", |
| 18 | + tags=["score-sets"], |
| 19 | + route_class=LoggedRoute, |
| 20 | + responses=shared_responses, |
| 21 | +) |
| 22 | +``` |
| 23 | + |
| 24 | +## Authentication |
| 25 | + |
| 26 | +Three tiers of auth dependency injection: |
| 27 | + |
| 28 | +| Dependency | Returns | Use When | |
| 29 | +|-----------|---------|----------| |
| 30 | +| `get_current_user` | `Optional[UserData]` | Public endpoints that behave differently for authenticated users | |
| 31 | +| `require_current_user` | `UserData` | Endpoints requiring login | |
| 32 | +| `require_current_user_with_email` | `UserData` | Endpoints requiring verified email (write operations) | |
| 33 | + |
| 34 | +Auth supports two mechanisms: |
| 35 | +- **ORCID JWT tokens** — primary auth for web users |
| 36 | +- **API keys** — for programmatic access |
| 37 | + |
| 38 | +```python |
| 39 | +@router.get("/{urn}") |
| 40 | +def get_score_set( |
| 41 | + urn: str, |
| 42 | + db: Session = Depends(get_db), |
| 43 | + user: Optional[UserData] = Depends(get_current_user), |
| 44 | +): |
| 45 | + ... |
| 46 | +``` |
| 47 | + |
| 48 | +## Authorization |
| 49 | + |
| 50 | +Permission checks use the `assert_permission()` helper with an `Action` enum: |
| 51 | + |
| 52 | +```python |
| 53 | +from mavedb.lib.authorization import assert_permission, Action |
| 54 | + |
| 55 | +assert_permission(user, item, Action.READ) # View |
| 56 | +assert_permission(user, item, Action.UPDATE) # Modify |
| 57 | +assert_permission(user, item, Action.DELETE) # Delete |
| 58 | +assert_permission(user, item, Action.ADD_ROLE) # Manage contributors |
| 59 | +``` |
| 60 | + |
| 61 | +Key authorization behaviors: |
| 62 | +- **Private resources return 404** (not 403) to prevent information leakage about existence |
| 63 | +- Permission logic dispatches by resource type (ExperimentSet, Experiment, ScoreSet, etc.) |
| 64 | +- Admins bypass most permission checks |
| 65 | + |
| 66 | +## Endpoint Patterns |
| 67 | + |
| 68 | +### Standard CRUD |
| 69 | +```python |
| 70 | +@router.get("/", response_model=list[ScoreSetShortModel]) |
| 71 | +def list_score_sets(db: Session = Depends(get_db)): ... |
| 72 | + |
| 73 | +@router.get("/{urn}", response_model=ScoreSetFullModel) |
| 74 | +def get_score_set(urn: str, db: Session = Depends(get_db)): ... |
| 75 | + |
| 76 | +@router.post("/", response_model=ScoreSetSavedModel, status_code=201) |
| 77 | +def create_score_set(body: ScoreSetCreateModel, db: Session = Depends(get_db)): ... |
| 78 | + |
| 79 | +@router.put("/{urn}", response_model=ScoreSetSavedModel) |
| 80 | +def update_score_set(urn: str, body: ScoreSetUpdateModel, db: Session = Depends(get_db)): ... |
| 81 | + |
| 82 | +@router.delete("/{urn}", status_code=204) |
| 83 | +def delete_score_set(urn: str, db: Session = Depends(get_db)): ... |
| 84 | +``` |
| 85 | + |
| 86 | +### Background Job Enqueueing |
| 87 | +For operations that trigger async processing: |
| 88 | +```python |
| 89 | +@router.post("/{urn}:publish") |
| 90 | +async def publish_score_set( |
| 91 | + urn: str, |
| 92 | + db: Session = Depends(get_db), |
| 93 | + user: UserData = Depends(require_current_user_with_email), |
| 94 | + worker: ArqRedis = Depends(get_worker), |
| 95 | +): |
| 96 | + # ... validation and DB updates ... |
| 97 | + await worker.enqueue_job( |
| 98 | + "create_variants_for_score_set", |
| 99 | + score_set.id, |
| 100 | + correlation_id, |
| 101 | + ) |
| 102 | +``` |
| 103 | + |
| 104 | +### Error Responses |
| 105 | +Shared error response definitions are used across routers: |
| 106 | +```python |
| 107 | +responses=shared_responses # Defines 4xx/5xx response schemas |
| 108 | +``` |
| 109 | + |
| 110 | +## Worker Integration |
| 111 | + |
| 112 | +### Job Pipeline |
| 113 | +Many operations chain through multiple worker jobs: |
| 114 | +1. `create_variants_for_score_set` — Parse uploaded CSV, create variant records |
| 115 | +2. `map_variants_for_score_set` — Map variants via DCD Mapping / VRS |
| 116 | +3. `submit_score_set_mappings_to_*` — Submit to ClinGen services |
| 117 | + |
| 118 | +### Job Patterns |
| 119 | +```python |
| 120 | +async def create_variants_for_score_set(ctx: dict, score_set_id: int, correlation_id: str): |
| 121 | + logging_context = setup_job_state(ctx, correlation_id) |
| 122 | + db = ctx["db"] |
| 123 | + |
| 124 | + try: |
| 125 | + # ... processing ... |
| 126 | + pass |
| 127 | + except Exception as e: |
| 128 | + send_slack_error(e, logging_context) |
| 129 | + raise |
| 130 | +``` |
| 131 | + |
| 132 | +### Backoff and Retry |
| 133 | +Use `enqueue_job_with_backoff()` for jobs that may need retries (e.g., external service calls). |
| 134 | + |
| 135 | +## Correlation IDs |
| 136 | +Every request gets a correlation ID via starlette-context middleware. Pass it to worker jobs for end-to-end request tracing: |
| 137 | +```python |
| 138 | +from mavedb.lib.logging.context import save_to_logging_context |
| 139 | +correlation_id = save_to_logging_context({"score_set_urn": urn}) |
| 140 | +``` |
0 commit comments