# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Purpose

`sr-delivery-youtube` delivers sound recordings to YouTube via SFTP using the DDEX standard. It is invoked as a step in a Step Function execution. Given a sound recording ID (and optionally a specific version), it fetches metadata from S3, generates DDEX XML, optionally fetches and transfers the audio asset, and uploads everything to YouTube's SFTP endpoint.

## Development Commands

All commands run from this directory (`lambda/sr-delivery-youtube/`):

```bash
# Run tests and linting
docker compose up --build lint-and-test

# Run with CI-style exit codes
docker compose up --exit-code-from lint-and-test --abort-on-container-exit --build lint-and-test

# Skip linting
SKIP_LINT=true docker compose up --build lint-and-test

# Run a specific test
TEST_ARGS="-k test_handler_with_asset" docker compose up --build lint-and-test

# Run with HTML coverage report (served on port 8000)
COV_REPORT=html docker compose up --build lint-and-test

# Start the function locally (port 9000)
docker compose up --build -d function

# Invoke locally
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d @tests/dev_sample_event.json

# Restart after code changes (no rebuild needed)
docker compose restart function
```

For local SFTP testing, use `docker-compose-dev.yaml` which spins up an `emberstack/sftp` container alongside the function. Configure `test-sftp.conf.json` with appropriate host keys before use.

## Architecture

### Delivery Flow

The handler (`src/app.py`) performs these steps in sequence:

1. Reads `sound_recording.id` and optionally `sound_recording.version` from the event. If `version` is provided, it fetches that specific OSR version from S3; otherwise it fetches the latest.
2. Deserializes the S3 JSON blob into an `OrchardSoundRecording` via `typedload` (with a custom enum handler that loads enum members by name rather than value — see `src/utils/typedload.py`).
3. Generates DDEX XML using `soundrecording_utils.ddex.generate.generate_ddex` with `RuleService.YOUTUBE` and the configured `DDEX_FILE_VERSION` (default `3.8`).
4. If `upload_asset=true` and `PROCESS_ASSET=true`, selects the best audio asset (`flac` preferred over `wav`) and downloads it from S3.
5. Constructs a batch directory structure: `<batch_id>/<isrc>/` for XML and `<batch_id>/<isrc>/resources/` for audio.
6. **Multi-ISRC handling**: YouTube requires one XML per ISRC. For each non-primary ISRC in `osr.track_connection.tracks`, a copy of the DDEX XML is created with the ISRC value substituted. All XML files are sent before the primary, and `delivery.complete` is sent last.
7. Writes the primary ISRC's XML to S3 audit storage.
8. Transfers all files to YouTube via SFTP. The `delivery.complete` marker (empty file) is always sent last, after all other files including the audio asset.

### SFTP Connection

`src/connectors/sftp_asset_delivery.py` maintains a module-level singleton `sftp_connection`. In `dev`, the private key comes from `config.YOUTUBE_SFTP_PRIVATE_KEY` (set via `.env`). In `qa`/`prod`, it is fetched from Secrets Manager under the key `YOUTUBE_CMS_FP_PRIVATE_KEY`. The final SFTP path is `<YOUTUBE_SFTP_BASE_DIRNAME>/<execution_name>/<subdir>`.

### Error Handling

`NoAssets` is raised (and re-raised without logging) when no `flac` or `wav` asset is found on the OSR — this reinitializes Sentry without a DSN (silencing the error in Sentry) before re-raising, so the Step Function can handle it as a known non-retryable condition. All other exceptions are logged and re-raised normally.

### GraphQL Utility

`src/utils/graphql.py` queries the GraphQL gateway for track metadata by ISRC. It is defined but not called from the main handler — it may be used for future enrichment or was used previously. `TransportServerError` with HTTP 502/503/504 is treated as a `RetryableException`.

## Event Schema

```json
{
  "sound_recording": {
    "id": "<uuid>",
    "version": "<version-id>"  // optional
  },
  "upload_asset": true,
  "execution_name": "<step-function-execution-id>",
  "delivery_type": "TAKEDOWN_DELIVERY"  // optional; triggers takedown DDEX
}
```

## Configuration (`.env` / `.env.shadow`)

Required env vars for local dev (copy `.env.shadow` to `.env`):

| Variable | Description |
|---|---|
| `ENVIRONMENT` | `dev` / `qa` / `prod` |
| `YOUTUBE_SFTP_HOSTNAME` | SFTP server hostname |
| `YOUTUBE_SFTP_PORT` | SFTP port (default 22) |
| `YOUTUBE_SFTP_USERNAME` | SFTP username |
| `YOUTUBE_SFTP_PRIVATE_KEY` | Private key string (dev only; QA/PROD use Secrets Manager) |
| `YOUTUBE_SFTP_BASE_DIRNAME` | Base directory on the SFTP server |
| `DDEX_FILE_VERSION` | DDEX schema version (default `3.8`) |
| `PROCESS_ASSET` | `true`/`false` — controls whether audio is uploaded (default `true`) |
| `YOUTUBE_INCLUDE_TESTING_HEADER` | Set `true` in dev compose to signal test deliveries |

## Linting

Runs `yamllint`, `mypy` (on `src/app.py` and `tests/unit/`), and `flake8`. Key flake8 settings: max line 120, single quotes, Google import order. Import order rule I100 is suppressed because `common` symlinked modules are treated as external.
