Skip to content

Commit ff8613a

Browse files
committed
Release v1.7.3
1 parent 0cb8cc3 commit ff8613a

7 files changed

Lines changed: 206 additions & 14 deletions

File tree

RELEASE_NOTES_v1.7.3.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## What's Changed
2+
3+
- Rewrite `resolve_entity` and `resolve_combination` HA API requests that receive FlyBase IDs to use the preferred VFB term name before querying Chado.
4+
- Return `NOT_FOUND` instead of passing raw IDs to Chado when no preferred name/label can be derived from VFB term info.
5+
- Add focused HA API validation tests covering query normalization, ID rewriting, and the no-fallback path.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
here = path.abspath(path.dirname(__file__))
55

6-
__version__ = "1.7.2"
6+
__version__ = "1.7.3"
77

88
# Get the long description from the README file
99
with open(path.join(here, 'README.md')) as f:

src/test/test_ha_api_validation.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Unit tests for HA API resolver query normalization and rewriting."""
2+
3+
import pytest
4+
5+
import vfbquery.ha_api as ha_api
6+
7+
8+
def test_parse_resolver_query_trims_whitespace():
9+
query = ha_api._parse_resolver_query(" P{VT054895-GAL4.DBD} ")
10+
11+
assert query == "P{VT054895-GAL4.DBD}"
12+
13+
14+
def test_rewrite_resolve_entity_query_uses_name_for_feature_ids(monkeypatch):
15+
class DummyVfb:
16+
def get_term_info(self, query, preview=False):
17+
assert query == "FBgn0000490"
18+
assert preview is False
19+
return {"Name": "dpp"}
20+
21+
monkeypatch.setattr(ha_api, "_vfb", DummyVfb(), raising=False)
22+
23+
query = ha_api._rewrite_resolve_entity_query("fbGN0000490")
24+
25+
assert query == "dpp"
26+
27+
28+
def test_rewrite_resolve_combination_query_uses_name_for_fbco_ids(monkeypatch):
29+
class DummyVfb:
30+
def get_term_info(self, query, preview=False):
31+
assert query == "FBco0000052"
32+
assert preview is False
33+
return {"Name": "GMR37H08-ZpGAL4DBD in attP2"}
34+
35+
monkeypatch.setattr(ha_api, "_vfb", DummyVfb(), raising=False)
36+
37+
query = ha_api._rewrite_resolve_combination_query("fbco0000052")
38+
39+
assert query == "GMR37H08-ZpGAL4DBD in attP2"
40+
41+
42+
def test_rewrite_resolve_entity_query_returns_none_when_no_term_info_name(monkeypatch):
43+
class DummyVfb:
44+
def get_term_info(self, query, preview=False):
45+
assert query == "FBst0007144"
46+
assert preview is False
47+
return None
48+
49+
monkeypatch.setattr(ha_api, "_vfb", DummyVfb(), raising=False)
50+
51+
query = ha_api._rewrite_resolve_entity_query("fbst0007144")
52+
53+
assert query is None
54+
55+
56+
def test_run_resolve_entity_returns_not_found_when_id_cannot_be_rewritten(monkeypatch):
57+
class DummyVfb:
58+
def get_term_info(self, query, preview=False):
59+
assert query == "FBst0007144"
60+
assert preview is False
61+
return None
62+
63+
def resolve_entity(self, query):
64+
raise AssertionError("Chado resolver should not receive raw IDs")
65+
66+
monkeypatch.setattr(ha_api, "_vfb", DummyVfb(), raising=False)
67+
68+
result = ha_api._run_resolve_entity("fbst0007144")
69+
70+
assert result == {"match_type": "NOT_FOUND", "results": []}
71+
72+
73+
def test_run_resolve_combination_returns_not_found_when_id_cannot_be_rewritten(monkeypatch):
74+
class DummyVfb:
75+
def get_term_info(self, query, preview=False):
76+
assert query == "FBco0000052"
77+
assert preview is False
78+
return None
79+
80+
def resolve_combination(self, query):
81+
raise AssertionError("Chado resolver should not receive raw IDs")
82+
83+
monkeypatch.setattr(ha_api, "_vfb", DummyVfb(), raising=False)
84+
85+
result = ha_api._run_resolve_combination("fbco0000052")
86+
87+
assert result == {"match_type": "NOT_FOUND", "results": []}
88+
89+
90+
def test_parse_resolver_query_requires_query():
91+
with pytest.raises(ValueError, match="Missing required parameter: query"):
92+
ha_api._parse_resolver_query(" ")

src/vfbquery/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,4 @@ def clear_solr_cache(query_type: str, term_id: str) -> bool:
9898
__solr_caching_available__ = False
9999

100100
# Version information
101-
__version__ = "1.7.2"
101+
__version__ = "1.7.3"

src/vfbquery/flybase_combo_pubs.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77

88

99
def resolve_combination(name_or_id):
10-
"""Resolve a combination name, synonym, or FBco ID.
10+
"""Resolve a combination name or synonym.
11+
12+
This Python helper still accepts FBco IDs for backwards compatibility.
13+
The HA API ``/resolve_combination`` endpoint prefers unresolved query text
14+
and, if an FBco ID slips through, rewrites it to the combination name via
15+
VFB term_info before resolving.
1116
1217
Resolution order:
1318
1. If FBco ID: direct lookup by feature.uniquename

src/vfbquery/flybase_stocks.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ def _run_query(conn, sql, params):
1818

1919

2020
def resolve_entity(name_or_id):
21-
"""Resolve a user-provided name, symbol, or FlyBase ID to a canonical FlyBase feature.
21+
"""Resolve a user-provided name or symbol to a canonical FlyBase feature.
22+
23+
This Python helper still accepts FlyBase IDs for backwards compatibility.
24+
The HA API ``/resolve_entity`` endpoint prefers unresolved query text and,
25+
if an ID slips through, rewrites it to the feature name via VFB term_info
26+
before resolving.
2227
2328
Resolution order for names (not IDs):
2429
1. Exact match on feature.name

src/vfbquery/ha_api.py

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
Endpoints (mirrors v3-cached.virtualflybrain.org):
1616
GET /get_term_info?id=<short_form>
1717
GET /run_query?id=<short_form>&query_type=<QueryType>
18+
GET /resolve_entity?query=<name_or_symbol> # IDs rewritten to names
19+
GET /find_stocks?id=<resolved_flybase_feature_id>
20+
GET /resolve_combination?query=<name_or_synonym> # IDs rewritten to names
21+
GET /find_combo_publications?id=<resolved_fbco_id>
22+
GET /list_connectome_datasets
23+
GET /query_connectivity?upstream_type=<name>&downstream_type=<name>
1824
GET /health
1925
GET /status — queue depth, cache stats & worker utilisation
2026
@@ -28,6 +34,7 @@
2834
import json
2935
import logging
3036
import os
37+
import re
3138
import sys
3239
import asyncio
3340
import time
@@ -53,6 +60,8 @@
5360
# or --workers CLI flag.
5461
DEFAULT_WORKERS = 10
5562
DEFAULT_MAX_QUEUE_DEPTH = 200
63+
_FLYBASE_FEATURE_ID_RE = re.compile(r"^FB(?:gn|al|ti|st|co)\d+$", re.IGNORECASE)
64+
_FBCO_ID_RE = re.compile(r"^FBco\d+$", re.IGNORECASE)
5665

5766

5867
# ---------------------------------------------------------------------------
@@ -317,7 +326,10 @@ def _run_query(short_form, func_name):
317326

318327
def _run_resolve_entity(name_or_id):
319328
"""Execute resolve_entity in a worker process."""
320-
return _vfb.resolve_entity(name_or_id)
329+
query = _rewrite_resolve_entity_query(name_or_id)
330+
if query is None:
331+
return {"match_type": "NOT_FOUND", "results": []}
332+
return _vfb.resolve_entity(query)
321333

322334

323335
def _run_find_stocks(feature_id, collection_filter):
@@ -327,7 +339,10 @@ def _run_find_stocks(feature_id, collection_filter):
327339

328340
def _run_resolve_combination(name_or_id):
329341
"""Execute resolve_combination in a worker process."""
330-
return _vfb.resolve_combination(name_or_id)
342+
query = _rewrite_resolve_combination_query(name_or_id)
343+
if query is None:
344+
return {"match_type": "NOT_FOUND", "results": []}
345+
return _vfb.resolve_combination(query)
331346

332347

333348
def _run_find_combo_publications(fbco_id):
@@ -368,6 +383,72 @@ def _convert_numpy_types(obj):
368383
return obj
369384

370385

386+
def _parse_resolver_query(query):
387+
"""Normalise a resolver query parameter."""
388+
normalized = (query or "").strip()
389+
if not normalized:
390+
raise ValueError("Missing required parameter: query")
391+
return normalized
392+
393+
394+
def _canonicalize_flybase_feature_id(feature_id):
395+
"""Return canonical FlyBase casing for a supported feature ID."""
396+
match = re.match(r"^FB(gn|al|ti|st|co)(\d+)$", feature_id or "", re.IGNORECASE)
397+
if not match:
398+
return feature_id
399+
return f"FB{match.group(1).lower()}{match.group(2)}"
400+
401+
402+
def _canonicalize_fbco_id(fbco_id):
403+
"""Return canonical FlyBase casing for an FBco ID."""
404+
match = re.match(r"^FBco(\d+)$", fbco_id or "", re.IGNORECASE)
405+
if not match:
406+
return fbco_id
407+
return f"FBco{match.group(1)}"
408+
409+
410+
def _preferred_term_info_query(term_info):
411+
"""Pick a stable name/label from VFB term_info."""
412+
if not isinstance(term_info, dict):
413+
return None
414+
415+
candidate = (term_info.get("Name") or "").strip()
416+
if candidate:
417+
return candidate
418+
419+
meta = term_info.get("Meta")
420+
if isinstance(meta, dict):
421+
for key in ("Symbol", "Name"):
422+
candidate = (meta.get(key) or "").strip()
423+
if candidate:
424+
match = re.match(r"^\[(.+)\]\([^)]+\)$", candidate)
425+
return match.group(1) if match else candidate
426+
427+
return None
428+
429+
430+
def _rewrite_resolve_entity_query(name_or_id):
431+
"""Rewrite a FlyBase feature ID to a preferred VFB term name."""
432+
query = _parse_resolver_query(name_or_id)
433+
if not _FLYBASE_FEATURE_ID_RE.match(query):
434+
return query
435+
436+
canonical_id = _canonicalize_flybase_feature_id(query)
437+
term_info = _vfb.get_term_info(canonical_id, preview=False)
438+
return _preferred_term_info_query(term_info)
439+
440+
441+
def _rewrite_resolve_combination_query(name_or_id):
442+
"""Rewrite an FBco ID to a preferred VFB term name."""
443+
query = _parse_resolver_query(name_or_id)
444+
if not _FBCO_ID_RE.match(query):
445+
return query
446+
447+
canonical_id = _canonicalize_fbco_id(query)
448+
term_info = _vfb.get_term_info(canonical_id, preview=False)
449+
return _preferred_term_info_query(term_info)
450+
451+
371452
# ---------------------------------------------------------------------------
372453
# HTTP handlers
373454
# ---------------------------------------------------------------------------
@@ -667,10 +748,12 @@ async def _dispatch_to_pool(request, cache_key, worker_fn, *args):
667748

668749

669750
async def handle_resolve_entity(request):
670-
"""GET /resolve_entity?query=<name_or_id>"""
671-
query = request.query.get("query")
672-
if not query:
673-
return web.json_response({"error": "Missing required parameter: query"}, status=400)
751+
"""GET /resolve_entity?query=<name_or_symbol>"""
752+
try:
753+
query = _parse_resolver_query(request.query.get("query"))
754+
except ValueError as exc:
755+
return web.json_response({"error": str(exc)}, status=400)
756+
674757
return await _dispatch_to_pool(
675758
request, f"resolve_entity:{query}", _run_resolve_entity, query
676759
)
@@ -689,10 +772,12 @@ async def handle_find_stocks(request):
689772

690773

691774
async def handle_resolve_combination(request):
692-
"""GET /resolve_combination?query=<name_or_id>"""
693-
query = request.query.get("query")
694-
if not query:
695-
return web.json_response({"error": "Missing required parameter: query"}, status=400)
775+
"""GET /resolve_combination?query=<name_or_synonym>"""
776+
try:
777+
query = _parse_resolver_query(request.query.get("query"))
778+
except ValueError as exc:
779+
return web.json_response({"error": str(exc)}, status=400)
780+
696781
return await _dispatch_to_pool(
697782
request, f"resolve_combination:{query}", _run_resolve_combination, query
698783
)

0 commit comments

Comments
 (0)