Skip to content

Commit c0114a8

Browse files
Add reproducible testing infrastructure
Uses testcontainers to spin up a reproducible test environment in a reasonable amount of time. 500 paths are sampled from real data to use for testing. These tests already exposed real bugs in the query code for counting and pathways. * Do not format or lint the dries/ directory * Added tests for the number of pathways from a specific tracer/gauge pair * Added tests for counting * Added tests for pathways counting * Set required environment variables in github CI to dummy values * Move the parse_literal function from submit to abomination * parse_literal now returns None on error * Generate tests based on sampled data * Added a test for total data size * Generate test data by randomly sampling real data * Run tests and type-checking in parallel with linting/formatting * Configure code coverage reporting * Correct strict typing issues
1 parent f350a7a commit c0114a8

32 files changed

Lines changed: 8264 additions & 2671 deletions

.github/workflows/server-pr.yaml

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,100 @@ on:
66
- 'server/**'
77

88
jobs:
9-
code-quality:
9+
type-check:
1010
runs-on: ubuntu-latest
1111
env:
12-
working-directory: ./server
12+
working-directory: ${{ github.workspace }}/server
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Setup python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: '3.13'
20+
21+
- name: Install Poetry
22+
uses: snok/install-poetry@v1
23+
with:
24+
virtualenvs-create: true
25+
virtualenvs-in-project: true
26+
virtualenvs-path: ${{ env.working-directory }}/.venv
27+
installer-parallel: true
28+
29+
- name: Load cached venv
30+
id: cached-poetry-dependencies
31+
uses: actions/cache@v4
32+
with:
33+
path: ${{ env.working-directory }}/.venv
34+
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock')}}
35+
36+
- name: Install dependencies
37+
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
38+
run: poetry install --no-interaction --no-root
39+
working-directory: ${{ env.working-directory }}
40+
41+
- name: Install project
42+
if: steps.cached-poetry-dependencies.outputs.cache-hit == 'true'
43+
run: poetry install --no-interaction
44+
working-directory: ${{ env.working-directory }}
45+
46+
- name: Type checking
47+
run: |
48+
source .venv/bin/activate
49+
mypy --strict ttfd/
50+
working-directory: ${{ env.working-directory }}
51+
52+
tests:
53+
runs-on: ubuntu-latest
54+
env:
55+
working-directory: ${{ github.workspace }}/server
56+
CLIENT_ID: ""
57+
CLIENT_SECRET: ""
58+
DATABASE_URI: ""
59+
steps:
60+
- uses: actions/checkout@v4
61+
62+
- name: Setup python
63+
uses: actions/setup-python@v5
64+
with:
65+
python-version: '3.13'
66+
67+
- name: Install Poetry
68+
uses: snok/install-poetry@v1
69+
with:
70+
virtualenvs-create: true
71+
virtualenvs-in-project: true
72+
virtualenvs-path: ${{ env.working-directory }}/.venv
73+
installer-parallel: true
74+
75+
- name: Load cached venv
76+
id: cached-poetry-dependencies
77+
uses: actions/cache@v4
78+
with:
79+
path: ${{ env.working-directory }}/.venv
80+
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock')}}
81+
82+
- name: Install dependencies
83+
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
84+
run: poetry install --no-interaction --no-root
85+
working-directory: ${{ env.working-directory }}
86+
87+
- name: Install project
88+
if: steps.cached-poetry-dependencies.outputs.cache-hit == 'true'
89+
run: poetry install --no-interaction
90+
working-directory: ${{ env.working-directory }}
91+
92+
- name: Run tests
93+
run: |
94+
source .venv/bin/activate
95+
coverage run -m pytest
96+
coverage report
97+
working-directory: ${{ env.working-directory }}
98+
99+
lint-format:
100+
runs-on: ubuntu-latest
101+
env:
102+
working-directory: ${{ github.workspace }}/server
13103
permissions:
14104
security-events: write
15105
actions: read
@@ -27,14 +117,14 @@ jobs:
27117
with:
28118
virtualenvs-create: true
29119
virtualenvs-in-project: true
30-
virtualenvs-path: .venv
120+
virtualenvs-path: ${{ env.working-directory }}/.venv
31121
installer-parallel: true
32122

33123
- name: Load cached venv
34124
id: cached-poetry-dependencies
35125
uses: actions/cache@v4
36126
with:
37-
path: .venv
127+
path: ${{ env.working-directory }}/.venv
38128
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock')}}
39129

40130
- name: Install dependencies
@@ -43,6 +133,7 @@ jobs:
43133
working-directory: ${{ env.working-directory }}
44134

45135
- name: Install project
136+
if: steps.cached-poetry-dependencies.outputs.cache-hit == 'true'
46137
run: poetry install --no-interaction
47138
working-directory: ${{ env.working-directory }}
48139

@@ -58,16 +149,8 @@ jobs:
58149
ruff check ttfd/
59150
working-directory: ${{ env.working-directory }}
60151

61-
# - name: Run tests
62-
# run: |
63-
# source .venv/bin/activate
64-
# pytest test/
65-
# coverage report
66-
# working-directory: ${{ env.working-directory }}
67-
68-
- name: Security
152+
- name: Security linting
69153
run: |
70154
source .venv/bin/activate
71155
bandit -c pyproject.toml -r ttfd
72156
working-directory: ${{ env.working-directory }}
73-

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ node_modules/
133133

134134
# Data
135135
*.db
136-
data/
136+
/server/data/
137137

138138
# Generated test files
139139
test/test_counts.py

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ services:
3434
ports:
3535
- "8000:8000"
3636
command: |
37-
bash -c 'poetry run alembic upgrade head && poetry run admin create -u 738 -n "James Collier" -i "https://services.vib.be/api/document/profilepic-url/53" && poetry run admin promote -u 738 && poetry run hypercorn --bind "0.0.0.0:8000" --reload ttfd.main:app'
37+
bash -c 'poetry run alembic upgrade head && poetry run admin create -u 738 -n "James Collier" -i "https://services.vib.be/api/document/profilepic-url/53" && poetry run admin promote -u 738 && poetry run hypercorn ttfd.main:app --bind "0.0.0.0:8000" --log-level=debug --reload --access-logfile=-'
3838
3939
volumes:
4040
pgdata: {}

server/dries/TTFD.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191

9292

9393
def do_the_thing(data,
94+
tracer_metabolite_id,
9495
gauge_metabolite_id,
9596
tracer_labeled_atoms,
9697
gauge_num_label_filter,
@@ -102,7 +103,7 @@ def do_the_thing(data,
102103
#i_f = open("data/ALPHA-GLUCOSE__L-LACTATE__30.newformat", 'r')
103104
tracer_labeled_atoms = [str(tla) for tla in tracer_labeled_atoms]
104105
path_list = []
105-
106+
106107
results = []
107108
for (idx, datum) in enumerate(data):
108109
reaction_info = datum.reaction_info
@@ -111,6 +112,11 @@ def do_the_thing(data,
111112
extended_reaction_list = reaction_info[2]
112113
metabolite_path = datum.metabolite_path
113114
metabolite_list = datum.metabolite_list
115+
if tracer_metabolite_id not in metabolite_path[0]:
116+
continue
117+
if gauge_metabolite_id not in metabolite_path[-1]:
118+
continue
119+
114120
end_metabolite_incorporations = metabolite_path[-1][gauge_metabolite_id][1]
115121
branch_info = datum.branch_info
116122
loop_info = datum.loop_info
@@ -308,4 +314,4 @@ def do_the_thing(data,
308314

309315

310316
if __name__ == '__main__':
311-
do_the_thing("L-LACTATE")
317+
do_the_thing("L-LACTATE")

server/dries/counts.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,29 @@
44
from dries.TTFD import do_the_thing
55

66

7-
def counts_per_label(data: List[Datum], end_metabolite, start: List[int]):
7+
def counts_per_label(data: List[Datum], tracer: str, gauge: str, start: List[int]):
88
counts = {}
9-
for label in range(1, 4):
10-
results = do_the_thing(data, end_metabolite, start, [label])
9+
for label in range(1, 50):
10+
results = do_the_thing(data, tracer, gauge, start, [label])
1111
if len(results) > 0:
1212
counts[label] = len(results)
1313

1414
return counts
1515

16+
def mklabeled(gauge: str, labeled_elements: list[int]) -> str:
17+
size = {
18+
"L-LACTATE": 6,
19+
"GLT": 10,
20+
"UMP": 21,
21+
"RIBULOSE-5P": 14,
22+
"PRO": 8,
23+
}[gauge]
24+
return "_".join("x" if p in labeled_elements else "" for p in range(size))
1625

1726
def counts_per_label_and_pathways(
1827
data: List[Datum],
19-
end_metabolite,
28+
tracer: str,
29+
gauge: str,
2030
start: List[int],
2131
label_count: int,
2232
labeled_elements: list[int] | None,
@@ -25,32 +35,35 @@ def counts_per_label_and_pathways(
2535
labeled = (
2636
None
2737
if labeled_elements is None
28-
else ["_".join(["x" if p in labeled_elements else "" for p in range(6)])]
38+
else [mklabeled(gauge, labeled_elements)]
2939
)
40+
3041
for pathway_count in range(1, 9):
3142
results = do_the_thing(
3243
data,
33-
end_metabolite,
34-
start,
35-
[label_count],
36-
[pathway_count],
37-
include_labeled_elements=labeled,
44+
tracer,
45+
gauge,
46+
start, # [1,2] (`tracer_labeled_atoms`)
47+
[label_count], # [2] (`gauge_num_label_filter`)
48+
[pathway_count], # [1] (`num_pathways_filter`)
49+
include_labeled_elements=labeled, # ["x_x____"] (`include_labeled_elements`)
3850
)
51+
# print(pathway_count, results)
3952
if len(results) > 0:
4053
counts[pathway_count] = len(results)
4154

4255
return counts
4356

4457

4558
def counts_per_position(
46-
data: List[Datum], end_metabolite, start: List[int], positions: List[int]
59+
data: List[Datum], tracer: str, gauge: str, start: List[int], positions: List[int]
4760
):
4861
labeling_mask = "_".join(["x" if p in positions else "" for p in range(6)])
4962
counts = {}
5063

5164
for pathway_count in range(1, 9):
5265
results = do_the_thing(
53-
data, end_metabolite, start, None, [pathway_count], [labeling_mask]
66+
data, tracer, gauge, start, None, [pathway_count], [labeling_mask]
5467
)
5568
if len(results) > 0:
5669
counts[pathway_count] = len(results)
@@ -59,7 +72,7 @@ def counts_per_position(
5972

6073

6174
def counts_per_labeled_position(
62-
data: List[Datum], end_metabolite, start: List[Tuple[int, int]]
75+
data: List[Datum], tracer, gauge, start: List[Tuple[int, int]]
6376
):
6477
labels, positions = zip(*start)
6578
labeling_mask = "_".join(
@@ -69,7 +82,7 @@ def counts_per_labeled_position(
6982

7083
for pathway_count in range(1, 9):
7184
results = do_the_thing(
72-
data, end_metabolite, start, None, [pathway_count], None, [labeling_mask]
85+
data, tracer, gauge, start, None, [pathway_count], None, [labeling_mask]
7386
)
7487
if len(results) > 0:
7588
counts[pathway_count] = len(results)

server/dries/preload.py

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class Datum:
1212
loop_info: Any
1313
pathway_info: Any
1414
expanded_reaction_paths: Any
15-
metabolite_list: List[Any]
15+
metabolite_list: list[Any]
1616

1717

1818
def expand_reaction_paths(compressed_reaction_paths):
@@ -22,42 +22,42 @@ def expand_reaction_paths(compressed_reaction_paths):
2222
return expanded_reaction_paths
2323

2424

25-
def preload(filename: str) -> List[Datum]:
25+
def preload(readable) -> list[Datum]:
2626
data = []
27-
with open(filename) as f:
28-
while True:
29-
reaction_line = f.readline().strip()
30-
if not reaction_line:
31-
break
32-
reaction_info = literal_eval(reaction_line)
33-
compressed_reaction_path = reaction_info[0]
34-
35-
metabolite_line = f.readline().strip()
36-
if not metabolite_line:
37-
break
38-
metabolite_path = literal_eval(metabolite_line)
39-
40-
branch_line = f.readline().strip()
41-
if not branch_line:
42-
break
43-
44-
loop_line = f.readline().strip()
45-
if not loop_line:
46-
break
47-
48-
pathways_line = f.readline().strip()
49-
if not pathways_line:
50-
break
51-
52-
data.append(
53-
Datum(
54-
reaction_info,
55-
metabolite_path,
56-
literal_eval(branch_line),
57-
literal_eval(loop_line),
58-
literal_eval(pathways_line),
59-
expand_reaction_paths(compressed_reaction_path),
60-
list(chain.from_iterable([list(m) for m in metabolite_path])),
61-
)
27+
28+
while True:
29+
reaction_line = readable.readline().strip()
30+
if not reaction_line:
31+
break
32+
reaction_info = literal_eval(reaction_line)
33+
compressed_reaction_path = reaction_info[0]
34+
35+
metabolite_line = readable.readline().strip()
36+
if not metabolite_line:
37+
break
38+
metabolite_path = literal_eval(metabolite_line)
39+
40+
branch_line = readable.readline().strip()
41+
if not branch_line:
42+
break
43+
44+
loop_line = readable.readline().strip()
45+
if not loop_line:
46+
break
47+
48+
pathways_line = readable.readline().strip()
49+
if not pathways_line:
50+
break
51+
52+
data.append(
53+
Datum(
54+
reaction_info,
55+
metabolite_path,
56+
literal_eval(branch_line),
57+
literal_eval(loop_line),
58+
literal_eval(pathways_line),
59+
expand_reaction_paths(compressed_reaction_path),
60+
list(chain.from_iterable([list(m) for m in metabolite_path])),
6261
)
62+
)
6363
return data

0 commit comments

Comments
 (0)