Skip to content

Commit f029bcb

Browse files
authored
Merge pull request #1244 from GenSpectrum/main
chore: update prod from main
2 parents 817fa31 + 881ebf2 commit f029bcb

31 files changed

Lines changed: 1616 additions & 528 deletions

.github/dependabot.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ updates:
66
interval: weekly
77
commit-message:
88
prefix: "chore(github-actions)"
9+
cooldown:
10+
default-days: 7
911
- package-ecosystem: npm
1012
directory: website/
1113
schedule:
@@ -16,6 +18,8 @@ updates:
1618
commit-message:
1719
prefix: "chore(website)"
1820
target-branch: "main"
21+
cooldown:
22+
default-days: 7
1923
- package-ecosystem: npm
2024
directory: website/
2125
schedule:
@@ -27,6 +31,8 @@ updates:
2731
- "patch"
2832
commit-message:
2933
prefix: "chore(website)"
34+
cooldown:
35+
default-days: 7
3036
- package-ecosystem: gradle
3137
directory: backend/
3238
schedule:
@@ -38,3 +44,5 @@ updates:
3844
- "patch"
3945
commit-message:
4046
prefix: "chore(backend)"
47+
cooldown:
48+
default-days: 7

.github/workflows/e2e.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,22 @@ jobs:
7070
env:
7171
BACKEND_URL: http://localhost:9021
7272

73+
- name: Collect container logs
74+
if: ${{ always() && !cancelled() }}
75+
run: |
76+
mkdir -p /tmp/container-logs
77+
services="$(docker compose -f ../docker-compose.yml config --services || true)"
78+
for service in $services; do
79+
docker compose -f ../docker-compose.yml logs --no-color "$service" > "/tmp/container-logs/${service}.log" || true
80+
done
81+
82+
- uses: actions/upload-artifact@v7
83+
if: ${{ always() && !cancelled() }}
84+
with:
85+
name: container-logs
86+
path: /tmp/container-logs/
87+
retention-days: 7
88+
7389
- uses: actions/upload-artifact@v7
7490
if: ${{ !cancelled() }}
7591
with:

.github/workflows/rebaseProd.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ jobs:
2626
echo "PR to update prod from main already exists - skipping creation"
2727
else
2828
body="This pull request updates the \`prod\` branch with the latest changes from the \`main\` branch.
29-
30-
### Make sure to merge this creating a merge commit.
31-
**Do not squash-merge** this PR. **Do not rebase and merge**."
32-
29+
30+
# ⚠️ Do not squash-merge! ⚠️
31+
Make sure to merge this creating a merge commit."
32+
3333
gh pr create --base prod --head main --title "chore: update prod from main" --body "$body"
3434
fi

collection-seeding/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Seeds the backend with example collections:
55
- **covid-resistance-mutations** — resistance mutation data for 3CLpro, RdRp, and Spike mAb
66
- **covid-pango-lineages** — one collection per pango lineage, with nucleotide substitutions as variants
77
- **covid-pango-lineages-sample** — same as above but limited to 10 lineages, for quick testing
8+
- **rsv-a-resistance-mutations** — RSV-A F protein resistance mutations against Nirsevimab and Palivizumab (fetched live from ViralZone)
9+
- **rsv-b-resistance-mutations** — RSV-B F protein resistance mutations against Nirsevimab and Palivizumab (fetched live from ViralZone)
810

911
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.
1012

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

3133
```bash
3234
pixi run seed # all sources
33-
pixi run seed-resistance # resistance mutations only
35+
pixi run seed-resistance # COVID resistance mutations only
3436
pixi run seed-lineages # pango lineages only
3537
pixi run seed-lineages-sample # first 10 pango lineages (quick test)
3638
```
3739

40+
RSV sources don't have dedicated tasks — use `--source` directly:
41+
42+
```bash
43+
pixi run seed --source rsv-a-resistance-mutations
44+
pixi run seed --source rsv-b-resistance-mutations
45+
```
46+
3847
To target a different backend:
3948

4049
```bash

collection-seeding/sources/pango_lineages.py renamed to collection-seeding/sources/covid_pango_lineages.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
)
1010

1111

12-
class PangoLineagesSource(Source):
12+
class CovidPangoLineagesSource(Source):
1313
"""Source: Pango lineage definitions from corneliusroemer/pango-sequences.
1414
1515
Creates one collection per lineage, with nucleotide substitutions as variants.
@@ -82,8 +82,8 @@ def _build_collection(self, entry: dict) -> Collection:
8282
}
8383

8484

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

8888
name = "covid-pango-lineages-sample"
8989
include_in_default_run = False

collection-seeding/sources/resistance_mutations.py renamed to collection-seeding/sources/covid_resistance_mutations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from sources import Source
33

44

5-
class ResistanceMutationsSource(Source):
5+
class CovidResistanceMutationsSource(Source):
66
"""Source: SARS-CoV-2 antiviral resistance mutations (ported from seed.mjs).
77
88
Three collections covering 3CLpro, RdRp, and Spike mAb resistance mutations

collection-seeding/sources/registry.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
place that needs to change — seed.py discovers sources exclusively through this list.
55
"""
66

7-
from sources.pango_lineages import PangoLineagesSource, PangoLineagesSampleSource
8-
from sources.resistance_mutations import ResistanceMutationsSource
7+
from sources.covid_pango_lineages import CovidPangoLineagesSource, CovidPangoLineagesSampleSource
8+
from sources.covid_resistance_mutations import CovidResistanceMutationsSource
9+
from sources.rsv_resistance_mutations import RsvAResistanceMutationsSource, RsvBResistanceMutationsSource
910
from sources import Source
1011

1112
ALL_SOURCES: list[type[Source]] = [
12-
ResistanceMutationsSource,
13-
PangoLineagesSource,
14-
PangoLineagesSampleSource,
13+
CovidResistanceMutationsSource,
14+
RsvAResistanceMutationsSource,
15+
RsvBResistanceMutationsSource,
16+
CovidPangoLineagesSource,
17+
CovidPangoLineagesSampleSource,
1518
]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import re
2+
import requests
3+
4+
from models import Collection, Variant
5+
from sources import Source
6+
7+
DATA_URL = "https://viralzone.expasy.org/resources/RSV/F_RSV_human.txt"
8+
9+
# Regex for row parsing. The file is a TSV file, but sometimes also uses spaces.
10+
# Columns 1 (A/B) and 2 (mutation) never contain spaces; column 3 (comment) may.
11+
_ROW_RE = re.compile(r"^\s*([AB])\s+(\S+)\s+(.+)")
12+
13+
14+
def _fetch_rows() -> list[tuple[str, list[str], str, str]]:
15+
response = requests.get(DATA_URL, timeout=60)
16+
response.raise_for_status()
17+
return _parse_rows(response.text)
18+
19+
20+
def _parse_rows(text: str) -> list[tuple[str, list[str], str, str]]:
21+
"""Parse ViralZone RSV resistance text into (rsv_type, aa_mutations, antibody, resistance_type) tuples."""
22+
rows = []
23+
for line in text.splitlines():
24+
m = _ROW_RE.match(line)
25+
if not m:
26+
continue
27+
rsv_type, aa_str, comment = m.group(1), m.group(2), m.group(3).strip()
28+
aa_mutations = [f"F:{part}" for part in aa_str.split("+")]
29+
if "Nirsevimab" in comment:
30+
antibody = "Nirsevimab"
31+
elif "Palivizumab" in comment:
32+
antibody = "Palivizumab"
33+
else:
34+
continue
35+
resistance_type = "Partial resistance" if "Partial resistance" in comment else "Resistance"
36+
rows.append((rsv_type, aa_mutations, antibody, resistance_type))
37+
return rows
38+
39+
40+
def _build_collections(rsv_type: str, organism: str, owned_tag: str) -> list[Collection]:
41+
all_rows = _fetch_rows()
42+
type_rows = [(aa, ab, res) for (t, aa, ab, res) in all_rows if t == rsv_type]
43+
collections = []
44+
for antibody in ("Nirsevimab", "Palivizumab"):
45+
variants: list[Variant] = [
46+
{
47+
"type": "filterObject",
48+
"name": res_type,
49+
"filterObject": {"aminoAcidMutations": aa},
50+
}
51+
for (aa, ab, res_type) in type_rows
52+
if ab == antibody
53+
]
54+
collections.append({
55+
"name": f"{antibody} resistance mutations",
56+
"organism": organism,
57+
"description": (
58+
f"RSV F protein resistance mutations against {antibody} "
59+
f"as per ViralZone (https://viralzone.expasy.org/11605). {owned_tag}"
60+
),
61+
"variants": variants,
62+
})
63+
return collections
64+
65+
66+
class RsvAResistanceMutationsSource(Source):
67+
name = "rsv-a-resistance-mutations"
68+
organism = "rsvA"
69+
owned_tag = "#resistance-mutation"
70+
71+
def get_collections(self) -> list[Collection]:
72+
return _build_collections("A", self.organism, self.owned_tag)
73+
74+
75+
class RsvBResistanceMutationsSource(Source):
76+
name = "rsv-b-resistance-mutations"
77+
organism = "rsvB"
78+
owned_tag = "#resistance-mutation"
79+
80+
def get_collections(self) -> list[Collection]:
81+
return _build_collections("B", self.organism, self.owned_tag)

collection-seeding/tests/test_pango_lineages.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import responses as rsps_lib
22

3-
from sources.pango_lineages import PangoLineagesSource, DATA_URL
3+
from sources.covid_pango_lineages import CovidPangoLineagesSource, DATA_URL
44

55
SAMPLE_DATA = {
66
"BA.2": {
@@ -40,39 +40,39 @@
4040

4141

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

4545

4646
# --- _build_collection ---
4747

4848

4949
def test_build_collection_basic():
50-
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
50+
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
5151
assert col["name"] == "BA.2"
5252
assert col["organism"] == "covid"
5353

5454

5555
def test_build_collection_description_format():
56-
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
56+
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
5757
assert "BA.2" in col["description"]
5858
assert "BA" in col["description"] # parent
5959
assert "22C" in col["description"] # clade
6060
assert "2022-01-20" in col["description"]
6161

6262

6363
def test_build_collection_missing_fields_use_defaults():
64-
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["XBB"])
64+
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["XBB"])
6565
assert "—" in col["description"] # parent and clade fallback
6666
assert "unknown" in col["description"] # date fallback
6767

6868

6969
def test_build_collection_always_four_variants():
70-
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
70+
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
7171
assert len(col["variants"]) == 4
7272

7373

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

8484

8585
def test_build_collection_variant_filter_keys():
86-
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
86+
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["BA.2"])
8787
variants = col["variants"]
8888
assert "nucleotideMutations" in variants[0]["filterObject"]
8989
assert "aminoAcidMutations" in variants[1]["filterObject"]
@@ -92,7 +92,7 @@ def test_build_collection_variant_filter_keys():
9292

9393

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

102102

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

110110

111111
def test_build_collection_empty_lists_when_all_blanks():
112-
col = PangoLineagesSource()._build_collection(SAMPLE_DATA["XBB"])
112+
col = CovidPangoLineagesSource()._build_collection(SAMPLE_DATA["XBB"])
113113
assert len(col["variants"]) == 4
114114
for v in col["variants"]:
115115
lists = list(v["filterObject"].values())
@@ -122,15 +122,15 @@ def test_build_collection_empty_lists_when_all_blanks():
122122
@rsps_lib.activate
123123
def test_get_collections_fetches_data_url():
124124
rsps_lib.add(rsps_lib.GET, DATA_URL, json=SAMPLE_DATA, status=200)
125-
PangoLineagesSource().get_collections()
125+
CovidPangoLineagesSource().get_collections()
126126
assert len(rsps_lib.calls) == 1
127127
assert rsps_lib.calls[0].request.url == DATA_URL
128128

129129

130130
@rsps_lib.activate
131131
def test_get_collections_includes_all_lineages():
132132
rsps_lib.add(rsps_lib.GET, DATA_URL, json=SAMPLE_DATA, status=200)
133-
cols = PangoLineagesSource().get_collections()
133+
cols = CovidPangoLineagesSource().get_collections()
134134
# All lineages included regardless of empty subs
135135
names = [c["name"] for c in cols]
136136
assert "BA.2" in names
@@ -141,12 +141,12 @@ def test_get_collections_includes_all_lineages():
141141
@rsps_lib.activate
142142
def test_get_collections_respects_limit():
143143
rsps_lib.add(rsps_lib.GET, DATA_URL, json=SAMPLE_DATA, status=200)
144-
cols = PangoLineagesSource(limit=1).get_collections()
144+
cols = CovidPangoLineagesSource(limit=1).get_collections()
145145
assert len(cols) <= 1
146146

147147

148148
@rsps_lib.activate
149149
def test_get_collections_no_limit_returns_all():
150150
rsps_lib.add(rsps_lib.GET, DATA_URL, json=SAMPLE_DATA, status=200)
151-
cols = PangoLineagesSource().get_collections()
151+
cols = CovidPangoLineagesSource().get_collections()
152152
assert len(cols) == 3

collection-seeding/tests/test_resistance_mutations.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from sources.resistance_mutations import ResistanceMutationsSource, _mature_name
1+
from sources.covid_resistance_mutations import CovidResistanceMutationsSource, _mature_name
22

33

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

77

88
# --- _mature_name ---
@@ -31,17 +31,17 @@ def test_mature_name_deletion():
3131

3232

3333
def test_get_collections_returns_three():
34-
cols = ResistanceMutationsSource().get_collections()
34+
cols = CovidResistanceMutationsSource().get_collections()
3535
assert len(cols) == 3
3636

3737

3838
def test_get_collections_all_covid():
39-
for col in ResistanceMutationsSource().get_collections():
39+
for col in CovidResistanceMutationsSource().get_collections():
4040
assert col["organism"] == "covid"
4141

4242

4343
def test_get_collections_variant_structure():
44-
for col in ResistanceMutationsSource().get_collections():
44+
for col in CovidResistanceMutationsSource().get_collections():
4545
assert col["variants"], f"'{col['name']}' has no variants"
4646
for v in col["variants"]:
4747
assert v["type"] == "filterObject"

0 commit comments

Comments
 (0)