Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ updates:
interval: weekly
commit-message:
prefix: "chore(github-actions)"
cooldown:
default-days: 7
- package-ecosystem: npm
directory: website/
schedule:
Expand All @@ -16,6 +18,8 @@ updates:
commit-message:
prefix: "chore(website)"
target-branch: "main"
cooldown:
default-days: 7
- package-ecosystem: npm
directory: website/
schedule:
Expand All @@ -27,6 +31,8 @@ updates:
- "patch"
commit-message:
prefix: "chore(website)"
cooldown:
default-days: 7
- package-ecosystem: gradle
directory: backend/
schedule:
Expand All @@ -38,3 +44,5 @@ updates:
- "patch"
commit-message:
prefix: "chore(backend)"
cooldown:
default-days: 7
16 changes: 16 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ jobs:
env:
BACKEND_URL: http://localhost:9021

- name: Collect container logs
if: ${{ always() && !cancelled() }}
run: |
mkdir -p /tmp/container-logs
services="$(docker compose -f ../docker-compose.yml config --services || true)"
for service in $services; do
docker compose -f ../docker-compose.yml logs --no-color "$service" > "/tmp/container-logs/${service}.log" || true
done

- uses: actions/upload-artifact@v7
if: ${{ always() && !cancelled() }}
with:
name: container-logs
path: /tmp/container-logs/
retention-days: 7

- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/rebaseProd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ jobs:
echo "PR to update prod from main already exists - skipping creation"
else
body="This pull request updates the \`prod\` branch with the latest changes from the \`main\` branch.
### Make sure to merge this creating a merge commit.
**Do not squash-merge** this PR. **Do not rebase and merge**."

# ⚠️ Do not squash-merge! ⚠️
Make sure to merge this creating a merge commit."

gh pr create --base prod --head main --title "chore: update prod from main" --body "$body"
fi
11 changes: 10 additions & 1 deletion collection-seeding/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Seeds the backend with example collections:
- **covid-resistance-mutations** — resistance mutation data for 3CLpro, RdRp, and Spike mAb
- **covid-pango-lineages** — one collection per pango lineage, with nucleotide substitutions as variants
- **covid-pango-lineages-sample** — same as above but limited to 10 lineages, for quick testing
- **rsv-a-resistance-mutations** — RSV-A F protein resistance mutations against Nirsevimab and Palivizumab (fetched live from ViralZone)
- **rsv-b-resistance-mutations** — RSV-B F protein resistance mutations against Nirsevimab and Palivizumab (fetched live from ViralZone)

The script is idempotent — re-running it will create new collections or update existing ones (matched by name). If a collection's name changes in the source, the old entry is orphaned and a new one is created.

Expand All @@ -30,11 +32,18 @@ Then use the provided tasks:

```bash
pixi run seed # all sources
pixi run seed-resistance # resistance mutations only
pixi run seed-resistance # COVID resistance mutations only
pixi run seed-lineages # pango lineages only
pixi run seed-lineages-sample # first 10 pango lineages (quick test)
```

RSV sources don't have dedicated tasks — use `--source` directly:

```bash
pixi run seed --source rsv-a-resistance-mutations
pixi run seed --source rsv-b-resistance-mutations
```

To target a different backend:

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
)


class PangoLineagesSource(Source):
class CovidPangoLineagesSource(Source):
"""Source: Pango lineage definitions from corneliusroemer/pango-sequences.

Creates one collection per lineage, with nucleotide substitutions as variants.
Expand Down Expand Up @@ -82,8 +82,8 @@ def _build_collection(self, entry: dict) -> Collection:
}


class PangoLineagesSampleSource(PangoLineagesSource):
"""Same as PangoLineagesSource but limited to the first 10 lineages, for quick testing."""
class CovidPangoLineagesSampleSource(CovidPangoLineagesSource):
"""Same as CovidPangoLineagesSource but limited to the first 10 lineages, for quick testing."""

name = "covid-pango-lineages-sample"
include_in_default_run = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sources import Source


class ResistanceMutationsSource(Source):
class CovidResistanceMutationsSource(Source):
"""Source: SARS-CoV-2 antiviral resistance mutations (ported from seed.mjs).

Three collections covering 3CLpro, RdRp, and Spike mAb resistance mutations
Expand Down
13 changes: 8 additions & 5 deletions collection-seeding/sources/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
place that needs to change — seed.py discovers sources exclusively through this list.
"""

from sources.pango_lineages import PangoLineagesSource, PangoLineagesSampleSource
from sources.resistance_mutations import ResistanceMutationsSource
from sources.covid_pango_lineages import CovidPangoLineagesSource, CovidPangoLineagesSampleSource
from sources.covid_resistance_mutations import CovidResistanceMutationsSource
from sources.rsv_resistance_mutations import RsvAResistanceMutationsSource, RsvBResistanceMutationsSource
from sources import Source

ALL_SOURCES: list[type[Source]] = [
ResistanceMutationsSource,
PangoLineagesSource,
PangoLineagesSampleSource,
CovidResistanceMutationsSource,
RsvAResistanceMutationsSource,
RsvBResistanceMutationsSource,
CovidPangoLineagesSource,
CovidPangoLineagesSampleSource,
]
81 changes: 81 additions & 0 deletions collection-seeding/sources/rsv_resistance_mutations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import re
import requests

from models import Collection, Variant
from sources import Source

DATA_URL = "https://viralzone.expasy.org/resources/RSV/F_RSV_human.txt"

# Regex for row parsing. The file is a TSV file, but sometimes also uses spaces.
# Columns 1 (A/B) and 2 (mutation) never contain spaces; column 3 (comment) may.
_ROW_RE = re.compile(r"^\s*([AB])\s+(\S+)\s+(.+)")


def _fetch_rows() -> list[tuple[str, list[str], str, str]]:
response = requests.get(DATA_URL, timeout=60)
response.raise_for_status()
return _parse_rows(response.text)


def _parse_rows(text: str) -> list[tuple[str, list[str], str, str]]:
"""Parse ViralZone RSV resistance text into (rsv_type, aa_mutations, antibody, resistance_type) tuples."""
rows = []
for line in text.splitlines():
m = _ROW_RE.match(line)
if not m:
continue
rsv_type, aa_str, comment = m.group(1), m.group(2), m.group(3).strip()
aa_mutations = [f"F:{part}" for part in aa_str.split("+")]
if "Nirsevimab" in comment:
antibody = "Nirsevimab"
elif "Palivizumab" in comment:
antibody = "Palivizumab"
else:
continue
resistance_type = "Partial resistance" if "Partial resistance" in comment else "Resistance"
rows.append((rsv_type, aa_mutations, antibody, resistance_type))
return rows


def _build_collections(rsv_type: str, organism: str, owned_tag: str) -> list[Collection]:
all_rows = _fetch_rows()
type_rows = [(aa, ab, res) for (t, aa, ab, res) in all_rows if t == rsv_type]
collections = []
for antibody in ("Nirsevimab", "Palivizumab"):
variants: list[Variant] = [
{
"type": "filterObject",
"name": res_type,
"filterObject": {"aminoAcidMutations": aa},
}
for (aa, ab, res_type) in type_rows
if ab == antibody
]
collections.append({
"name": f"{antibody} resistance mutations",
"organism": organism,
"description": (
f"RSV F protein resistance mutations against {antibody} "
f"as per ViralZone (https://viralzone.expasy.org/11605). {owned_tag}"
),
"variants": variants,
})
return collections


class RsvAResistanceMutationsSource(Source):
name = "rsv-a-resistance-mutations"
organism = "rsvA"
owned_tag = "#resistance-mutation"

def get_collections(self) -> list[Collection]:
return _build_collections("A", self.organism, self.owned_tag)


class RsvBResistanceMutationsSource(Source):
name = "rsv-b-resistance-mutations"
organism = "rsvB"
owned_tag = "#resistance-mutation"

def get_collections(self) -> list[Collection]:
return _build_collections("B", self.organism, self.owned_tag)
30 changes: 15 additions & 15 deletions collection-seeding/tests/test_pango_lineages.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import responses as rsps_lib

from sources.pango_lineages import PangoLineagesSource, DATA_URL
from sources.covid_pango_lineages import CovidPangoLineagesSource, DATA_URL

SAMPLE_DATA = {
"BA.2": {
Expand Down Expand Up @@ -40,39 +40,39 @@


def test_name():
assert PangoLineagesSource.name == "covid-pango-lineages"
assert CovidPangoLineagesSource.name == "covid-pango-lineages"


# --- _build_collection ---


def test_build_collection_basic():
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
assert col["name"] == "BA.2"
assert col["organism"] == "covid"


def test_build_collection_description_format():
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
assert "BA.2" in col["description"]
assert "BA" in col["description"] # parent
assert "22C" in col["description"] # clade
assert "2022-01-20" in col["description"]


def test_build_collection_missing_fields_use_defaults():
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["XBB"])
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["XBB"])
assert "—" in col["description"] # parent and clade fallback
assert "unknown" in col["description"] # date fallback


def test_build_collection_always_four_variants():
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
assert len(col["variants"]) == 4


def test_build_collection_variant_names():
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
names = [v["name"] for v in col["variants"]]
assert names == [
"Nucleotide substitutions",
Expand All @@ -83,7 +83,7 @@ def test_build_collection_variant_names():


def test_build_collection_variant_filter_keys():
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
variants = col["variants"]
assert "nucleotideMutations" in variants[0]["filterObject"]
assert "aminoAcidMutations" in variants[1]["filterObject"]
Expand All @@ -92,7 +92,7 @@ def test_build_collection_variant_filter_keys():


def test_build_collection_variant_contents():
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
variants = col["variants"]
assert variants[0]["filterObject"]["nucleotideMutations"] == ["C241T", "A23403G"]
assert variants[1]["filterObject"]["aminoAcidMutations"] == ["S:N501Y"]
Expand All @@ -101,15 +101,15 @@ def test_build_collection_variant_contents():


def test_build_collection_filters_blank_subs():
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
# nucSubstitutions has ["C241T", "A23403G", ""] — blank should be dropped
nuc = col["variants"][0]["filterObject"]["nucleotideMutations"]
assert "" not in nuc
assert len(nuc) == 2


def test_build_collection_empty_lists_when_all_blanks():
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["XBB"])
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["XBB"])
assert len(col["variants"]) == 4
for v in col["variants"]:
lists = list(v["filterObject"].values())
Expand All @@ -122,15 +122,15 @@ def test_build_collection_empty_lists_when_all_blanks():
@rsps_lib.activate
def test_get_collections_fetches_data_url():
rsps_lib.add(rsps_lib.GET, DATA_URL, json=SAMPLE_DATA, status=200)
PangoLineagesSource().get_collections()
CovidPangoLineagesSource().get_collections()
assert len(rsps_lib.calls) == 1
assert rsps_lib.calls[0].request.url == DATA_URL


@rsps_lib.activate
def test_get_collections_includes_all_lineages():
rsps_lib.add(rsps_lib.GET, DATA_URL, json=SAMPLE_DATA, status=200)
cols = PangoLineagesSource().get_collections()
cols = CovidPangoLineagesSource().get_collections()
# All lineages included regardless of empty subs
names = [c["name"] for c in cols]
assert "BA.2" in names
Expand All @@ -141,12 +141,12 @@ def test_get_collections_includes_all_lineages():
@rsps_lib.activate
def test_get_collections_respects_limit():
rsps_lib.add(rsps_lib.GET, DATA_URL, json=SAMPLE_DATA, status=200)
cols = PangoLineagesSource(limit=1).get_collections()
cols = CovidPangoLineagesSource(limit=1).get_collections()
assert len(cols) <= 1


@rsps_lib.activate
def test_get_collections_no_limit_returns_all():
rsps_lib.add(rsps_lib.GET, DATA_URL, json=SAMPLE_DATA, status=200)
cols = PangoLineagesSource().get_collections()
cols = CovidPangoLineagesSource().get_collections()
assert len(cols) == 3
10 changes: 5 additions & 5 deletions collection-seeding/tests/test_resistance_mutations.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from sources.resistance_mutations import ResistanceMutationsSource, _mature_name
from sources.covid_resistance_mutations import CovidResistanceMutationsSource, _mature_name


def test_name():
assert ResistanceMutationsSource.name == "covid-resistance-mutations"
assert CovidResistanceMutationsSource.name == "covid-resistance-mutations"


# --- _mature_name ---
Expand Down Expand Up @@ -31,17 +31,17 @@ def test_mature_name_deletion():


def test_get_collections_returns_three():
cols = ResistanceMutationsSource().get_collections()
cols = CovidResistanceMutationsSource().get_collections()
assert len(cols) == 3


def test_get_collections_all_covid():
for col in ResistanceMutationsSource().get_collections():
for col in CovidResistanceMutationsSource().get_collections():
assert col["organism"] == "covid"


def test_get_collections_variant_structure():
for col in ResistanceMutationsSource().get_collections():
for col in CovidResistanceMutationsSource().get_collections():
assert col["variants"], f"'{col['name']}' has no variants"
for v in col["variants"]:
assert v["type"] == "filterObject"
Expand Down
Loading