Skip to content

Commit 24cebf5

Browse files
committed
Fix CI test collection failure caused by eager KG client initialisation
Move the KG client initialisation from module-level in data_models.py to a FastAPI lifespan handler in main.py, so test collection in CI does not require KG credentials, while the service still fails fast on startup if they are missing. The term_cache population and Enum construction are also made resilient to missing credentials: get_term_cache() is called at module level but wrapped in a try/except catching ValueError and AuthenticationError, and all Enum definitions use .get() so they produce empty Enums rather than raising KeyError when the cache is empty. In production, credentials are present so the module-level call succeeds and the cache is fully populated before the server begins serving requests.
1 parent c4c2655 commit 24cebf5

3 files changed

Lines changed: 28 additions & 16 deletions

File tree

.gitlab-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ run_tests:
1212
script:
1313
- cd validation_service_api
1414
- pip install --no-cache-dir -r requirements.txt.lock
15-
- pip install pytest
15+
- pip install pytest pytest-asyncio
1616
- pytest validation_service/tests
1717
tags:
1818
- docker-runner

validation_service_api/validation_service/data_models.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
from .auth import get_kg_client_for_service_account
3232

3333

34-
kg_service_client = get_kg_client_for_service_account()
3534
term_cache = {}
3635

3736
logger = logging.getLogger("validation_service_api")
@@ -76,7 +75,7 @@ def filter_study_targets(study_targets):
7675
brain_regions = []
7776
species = []
7877
for item in as_list(study_targets):
79-
item = item.resolve(kg_service_client, release_status="any")
78+
item = item.resolve(get_kg_client_for_service_account(), release_status="any")
8079
if isinstance(item, omterms.CellType):
8180
cell_types.append(item.name)
8281
elif isinstance(item, omterms.UBERONParcellation):
@@ -111,14 +110,18 @@ def get_term_cache():
111110
omterms.Service,
112111
omterms.ActionStatusType
113112
):
114-
objects = cls.list(kg_service_client, api="core", release_status="any", size=10000)
113+
objects = cls.list(get_kg_client_for_service_account(), api="core", release_status="any", size=10000)
115114
term_cache[cls.__name__] = {
116115
"names": {obj.name: obj for obj in objects},
117116
"ids": {obj.id: obj for obj in objects}
118117
}
119118
return term_cache
120119

121-
get_term_cache()
120+
121+
try:
122+
get_term_cache()
123+
except (ValueError, AuthenticationError):
124+
pass # term_cache stays empty; Enums will have no members until the server starts
122125

123126

124127
def get_term(cls_name, attr):
@@ -143,7 +146,7 @@ def get_term_name_from_id(cls_name, attr):
143146
"Species",
144147
[
145148
(name.replace(" ", "_"), name)
146-
for name in sorted(term_cache["Species"]["names"])
149+
for name in sorted(term_cache.get("Species", {}).get("names", {}))
147150
]
148151
)
149152

@@ -152,7 +155,7 @@ def get_term_name_from_id(cls_name, attr):
152155
"BrainRegion",
153156
[
154157
(name.replace(" ", "_"), name)
155-
for name in sorted(term_cache["UBERONParcellation"]["names"])
158+
for name in sorted(term_cache.get("UBERONParcellation", {}).get("names", {}))
156159
]
157160
)
158161

@@ -161,7 +164,7 @@ def get_term_name_from_id(cls_name, attr):
161164
"ModelScope",
162165
[
163166
(name.replace(" ", "_").replace(":", "__"), name)
164-
for name in sorted(term_cache["ModelScope"]["names"])
167+
for name in sorted(term_cache.get("ModelScope", {}).get("names", {}))
165168
],
166169
)
167170

@@ -170,7 +173,7 @@ def get_term_name_from_id(cls_name, attr):
170173
"AbstractionLevel",
171174
[
172175
(name.replace(" ", "_").replace(":", "__"), name)
173-
for name in sorted(term_cache["ModelAbstractionLevel"]["names"])
176+
for name in sorted(term_cache.get("ModelAbstractionLevel", {}).get("names", {}))
174177
],
175178
)
176179

@@ -179,7 +182,7 @@ def get_term_name_from_id(cls_name, attr):
179182
"CellType",
180183
[
181184
(name.replace(" ", "_"), name)
182-
for name in sorted(term_cache["CellType"]["names"])
185+
for name in sorted(term_cache.get("CellType", {}).get("names", {}))
183186
]
184187
)
185188

@@ -193,7 +196,7 @@ def get_identifier(iri, prefix):
193196
"ContentType",
194197
[
195198
(get_identifier(obj.uuid, "ct"), obj.name)
196-
for obj in sorted(term_cache["ContentType"]["names"].values(), key=lambda obj: obj.name)
199+
for obj in sorted(term_cache.get("ContentType", {}).get("names", {}).values(), key=lambda obj: obj.name)
197200
]
198201
)
199202

@@ -209,7 +212,7 @@ class ImplementationStatus(str, Enum):
209212
"RecordingModality",
210213
[
211214
(name.replace(" ", "_"), name)
212-
for name in sorted(term_cache["Technique"]["names"])
215+
for name in sorted(term_cache.get("Technique", {}).get("names", {}))
213216
# or could use ExperimentalApproach
214217
# ideally should filter techniques to include only those are recording techniques
215218
]
@@ -229,7 +232,7 @@ class ImplementationStatus(str, Enum):
229232
"ScoreType",
230233
[
231234
(name.replace(" ", "_"), name)
232-
for name in sorted(term_cache["DifferenceMeasure"]["names"])
235+
for name in sorted(term_cache.get("DifferenceMeasure", {}).get("names", {}))
233236
]
234237
)
235238

@@ -238,7 +241,7 @@ class ImplementationStatus(str, Enum):
238241
"License",
239242
[
240243
(name.replace(" ", "_"), name)
241-
for name in sorted(term_cache["License"]["names"])
244+
for name in sorted(term_cache.get("License", {}).get("names", {}))
242245
]
243246
)
244247

@@ -247,7 +250,7 @@ class ImplementationStatus(str, Enum):
247250
"ActionStatusType",
248251
[
249252
(name.replace(" ", "_"), name)
250-
for name in sorted(term_cache["ActionStatusType"]["names"])
253+
for name in sorted(term_cache.get("ActionStatusType", {}).get("names", {}))
251254
]
252255
)
253256

validation_service_api/validation_service/main.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
from contextlib import asynccontextmanager
2+
13
from fastapi import FastAPI
24
from starlette.middleware.sessions import SessionMiddleware
35
from starlette.middleware.cors import CORSMiddleware
46

57
from .resources import models, tests, vocab, results, auth, comments
68
from . import settings
9+
from .auth import get_kg_client_for_service_account
710

811

912
description = """
@@ -31,7 +34,13 @@
3134
description = warning_message + description
3235

3336

34-
app = FastAPI(title="EBRAINS Model Validation Service", description=description, version="3beta")
37+
@asynccontextmanager
38+
async def lifespan(app: FastAPI):
39+
get_kg_client_for_service_account() # fail fast if credentials are missing
40+
yield
41+
42+
43+
app = FastAPI(title="EBRAINS Model Validation Service", description=description, version="3beta", lifespan=lifespan)
3544

3645
app.add_middleware(
3746
SessionMiddleware,

0 commit comments

Comments
 (0)