Skip to content

Commit 9bdd8e1

Browse files
Robbie1977claude
andcommitted
Add FlyBase stock finder, combination publications, and VFB neuron connectivity modules
Integrates three ask-vfb skills as importable Python modules: - flybase_stocks: resolve_entity() + find_stocks() for gene/allele/insertion/combination stock lookup - flybase_combo_pubs: resolve_combination() + find_combo_publications() for split system combination publications - vfb_connectivity: list_connectome_datasets() + query_connectivity() for class-level synaptic connectivity Uses VFBquery's own Neo4jConnect for connectivity queries (no vfb_connect dependency). Adds psycopg[binary] dependency for FlyBase Chado PostgreSQL access. Exposes all functions via ha_api.py HTTP endpoints with caching/coalescing support. Includes 45 integration tests against live FlyBase and VFB databases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 23e91a5 commit 9bdd8e1

11 files changed

Lines changed: 1509 additions & 4 deletions

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ dacite
44
requests
55
pysolr
66
get_version
7-
aiohttp
7+
aiohttp
8+
psycopg[binary]>=3.0

setup.py

Lines changed: 2 additions & 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.6.13"
6+
__version__ = "1.7.0"
77

88
# Get the long description from the README file
99
with open(path.join(here, 'README.md')) as f:
@@ -35,6 +35,7 @@
3535
"dataclasses-json",
3636
"dacite",
3737
"requests",
38+
"psycopg[binary]>=3.0",
3839
],
3940
python_requires=">=3.7",
4041
project_urls={ # Optional
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Tests for flybase_combo_pubs module — combination resolution and publication lookup."""
2+
import pytest
3+
4+
from vfbquery.flybase_combo_pubs import resolve_combination, find_combo_publications
5+
6+
KNOWN_COMBO_ID = "FBco0000052"
7+
KNOWN_COMBO_SYNONYM = "MB002B"
8+
KNOWN_PUB_FBRF = "FBrf0227179"
9+
NONEXISTENT_COMBO = "NONEXISTENT_COMBO_XYZ"
10+
11+
12+
# ---------------------------------------------------------------------------
13+
# resolve_combination tests
14+
# ---------------------------------------------------------------------------
15+
16+
17+
class TestResolveCombinationByID:
18+
@pytest.mark.integration
19+
def test_known_combo_id(self):
20+
result = resolve_combination(KNOWN_COMBO_ID)
21+
assert result["match_type"] == "EXACT"
22+
assert any(r["uniquename"] == KNOWN_COMBO_ID for r in result["results"])
23+
24+
25+
class TestResolveCombinationSynonym:
26+
@pytest.mark.integration
27+
def test_synonym_resolves(self):
28+
result = resolve_combination(KNOWN_COMBO_SYNONYM)
29+
assert result["match_type"] == "SYNONYM"
30+
assert any(r["uniquename"] == KNOWN_COMBO_ID for r in result["results"])
31+
32+
@pytest.mark.integration
33+
def test_synonym_shows_matched_synonym(self):
34+
result = resolve_combination(KNOWN_COMBO_SYNONYM)
35+
assert any(
36+
r.get("matched_synonym") == KNOWN_COMBO_SYNONYM
37+
for r in result["results"]
38+
)
39+
40+
41+
class TestResolveCombinationBroadMatch:
42+
@pytest.mark.integration
43+
def test_broad_match_partial_name(self):
44+
result = resolve_combination("R14C08")
45+
assert result["match_type"] in ("EXACT", "SYNONYM", "BROAD")
46+
assert any("FBco" in r["uniquename"] for r in result["results"])
47+
48+
49+
class TestResolveCombinationNotFound:
50+
@pytest.mark.integration
51+
def test_nonexistent_name(self):
52+
result = resolve_combination(NONEXISTENT_COMBO)
53+
assert result["match_type"] == "NOT_FOUND"
54+
55+
56+
# ---------------------------------------------------------------------------
57+
# find_combo_publications tests
58+
# ---------------------------------------------------------------------------
59+
60+
61+
class TestFindComboPublications:
62+
@pytest.mark.integration
63+
def test_find_pubs_for_known_combo(self):
64+
pubs = find_combo_publications(KNOWN_COMBO_ID)
65+
assert len(pubs) > 0
66+
assert any(p["fbrf"] == KNOWN_PUB_FBRF for p in pubs)
67+
68+
@pytest.mark.integration
69+
def test_pubs_have_expected_keys(self):
70+
pubs = find_combo_publications(KNOWN_COMBO_ID)
71+
assert len(pubs) > 0
72+
expected_keys = {"fbrf", "title", "year", "miniref", "pub_type", "doi", "pmid", "pmcid"}
73+
for p in pubs:
74+
assert expected_keys.issubset(p.keys())
75+
76+
@pytest.mark.integration
77+
def test_pubs_have_doi(self):
78+
pubs = find_combo_publications(KNOWN_COMBO_ID)
79+
has_doi = any(p.get("doi", "").startswith("10.") for p in pubs)
80+
assert has_doi, "Expected at least one publication with a DOI"
81+
82+
83+
class TestFindComboPublicationsEdgeCases:
84+
@pytest.mark.integration
85+
def test_nonexistent_combo(self):
86+
pubs = find_combo_publications("FBco9999999")
87+
assert pubs == []
88+
89+
def test_invalid_id_prefix(self):
90+
with pytest.raises(ValueError, match="Expected FBco"):
91+
find_combo_publications("FBgn0000490")

src/test/test_flybase_stocks.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""Tests for flybase_stocks module — entity resolution and stock discovery."""
2+
import pytest
3+
4+
from vfbquery.flybase_stocks import resolve_entity, find_stocks
5+
6+
# Known stable test entities
7+
KNOWN_GENE_SYMBOL = "dpp"
8+
KNOWN_GENE_ID = "FBgn0000490"
9+
KNOWN_GENE_ID_2 = "FBgn0003996" # white
10+
KNOWN_SYNONYM = "CG9885"
11+
KNOWN_SYNONYM_RESOLVES_TO = "dpp"
12+
NONEXISTENT_ENTITY = "xyzzy_nonexistent_gene_99999"
13+
14+
15+
# ---------------------------------------------------------------------------
16+
# resolve_entity tests
17+
# ---------------------------------------------------------------------------
18+
19+
20+
class TestResolveEntityByID:
21+
@pytest.mark.integration
22+
def test_known_gene_id(self):
23+
result = resolve_entity(KNOWN_GENE_ID)
24+
assert result["match_type"] == "EXACT"
25+
assert len(result["results"]) > 0
26+
assert result["results"][0]["uniquename"] == KNOWN_GENE_ID
27+
assert result["results"][0]["type"] == "gene"
28+
29+
@pytest.mark.integration
30+
def test_known_combo_id(self):
31+
result = resolve_entity("FBco0001000")
32+
assert result["match_type"] == "EXACT"
33+
assert result["results"][0]["uniquename"] == "FBco0001000"
34+
assert result["results"][0]["type"] == "split system combination"
35+
36+
@pytest.mark.integration
37+
def test_nonexistent_id(self):
38+
result = resolve_entity("FBgn9999999999")
39+
assert result["match_type"] == "NOT_FOUND"
40+
assert result["results"] == []
41+
42+
43+
class TestResolveEntityExactMatch:
44+
@pytest.mark.integration
45+
def test_exact_gene_name(self):
46+
result = resolve_entity(KNOWN_GENE_SYMBOL)
47+
assert result["match_type"] == "EXACT"
48+
assert any(r["uniquename"] == KNOWN_GENE_ID for r in result["results"])
49+
assert any(r["type"] == "gene" for r in result["results"])
50+
51+
52+
class TestResolveEntitySynonym:
53+
@pytest.mark.integration
54+
def test_synonym_resolves(self):
55+
result = resolve_entity(KNOWN_SYNONYM)
56+
assert result["match_type"] == "SYNONYM"
57+
assert any(
58+
KNOWN_SYNONYM_RESOLVES_TO in r["name"] for r in result["results"]
59+
)
60+
61+
@pytest.mark.integration
62+
def test_synonym_includes_matched_synonym(self):
63+
result = resolve_entity(KNOWN_SYNONYM)
64+
assert any(
65+
r["matched_synonym"] == KNOWN_SYNONYM for r in result["results"]
66+
)
67+
68+
@pytest.mark.integration
69+
def test_combo_synonym_resolves(self):
70+
result = resolve_entity("MB002B")
71+
assert result["match_type"] == "SYNONYM"
72+
assert any("FBco" in r["uniquename"] for r in result["results"])
73+
74+
75+
class TestResolveEntityBroadMatch:
76+
@pytest.mark.integration
77+
def test_broad_match_partial_name(self):
78+
result = resolve_entity("Scer\\GAL4")
79+
assert result["match_type"] in ("EXACT", "SYNONYM", "BROAD")
80+
assert len(result["results"]) > 0
81+
82+
83+
class TestResolveEntityNotFound:
84+
@pytest.mark.integration
85+
def test_nonexistent_name(self):
86+
result = resolve_entity(NONEXISTENT_ENTITY)
87+
assert result["match_type"] == "NOT_FOUND"
88+
89+
90+
# ---------------------------------------------------------------------------
91+
# find_stocks tests
92+
# ---------------------------------------------------------------------------
93+
94+
95+
class TestFindStocksGene:
96+
@pytest.mark.integration
97+
def test_dpp_returns_stocks(self):
98+
stocks = find_stocks(KNOWN_GENE_ID)
99+
assert len(stocks) > 0
100+
assert all("stock_id" in s for s in stocks)
101+
102+
@pytest.mark.integration
103+
def test_dpp_stocks_have_fbst(self):
104+
stocks = find_stocks(KNOWN_GENE_ID)
105+
assert any(s["stock_id"].startswith("FBst") for s in stocks)
106+
107+
@pytest.mark.integration
108+
def test_white_returns_stocks(self):
109+
stocks = find_stocks(KNOWN_GENE_ID_2)
110+
assert len(stocks) > 0
111+
112+
113+
class TestFindStocksCollectionFilter:
114+
@pytest.mark.integration
115+
def test_bloomington_filter(self):
116+
stocks = find_stocks(KNOWN_GENE_ID, collection_filter="Bloomington")
117+
assert len(stocks) > 0
118+
for s in stocks:
119+
if s.get("collection"):
120+
assert "Bloomington" in s["collection"]
121+
122+
@pytest.mark.integration
123+
def test_filter_reduces_count(self):
124+
all_stocks = find_stocks(KNOWN_GENE_ID)
125+
filtered = find_stocks(KNOWN_GENE_ID, collection_filter="Bloomington")
126+
assert len(filtered) <= len(all_stocks)
127+
128+
129+
class TestFindStocksAllele:
130+
@pytest.mark.integration
131+
def test_known_allele(self):
132+
# dpp[hr4] = FBal0000469
133+
stocks = find_stocks("FBal0000469")
134+
assert isinstance(stocks, list)
135+
136+
137+
class TestFindStocksInsertion:
138+
@pytest.mark.integration
139+
def test_known_insertion(self):
140+
stocks = find_stocks("FBti0016417")
141+
assert len(stocks) > 0
142+
143+
144+
class TestFindStocksStockDetail:
145+
@pytest.mark.integration
146+
def test_stock_lookup(self):
147+
stocks = find_stocks("FBst0007144")
148+
assert len(stocks) > 0
149+
assert any("7144" in str(s.get("stock_number", "")) for s in stocks)
150+
151+
@pytest.mark.integration
152+
def test_stock_includes_collection(self):
153+
stocks = find_stocks("FBst0007144")
154+
assert any("Bloomington" in str(s.get("collection", "")) for s in stocks)
155+
156+
157+
class TestFindStocksCombination:
158+
@pytest.mark.integration
159+
def test_known_combination(self):
160+
stocks = find_stocks("FBco0001000")
161+
assert len(stocks) > 0
162+
assert all("stock_id" in s for s in stocks)
163+
164+
@pytest.mark.integration
165+
def test_combination_has_component(self):
166+
stocks = find_stocks("FBco0001000")
167+
assert any("component" in s for s in stocks)
168+
169+
@pytest.mark.integration
170+
def test_nonexistent_combination(self):
171+
stocks = find_stocks("FBco9999999")
172+
assert stocks == []
173+
174+
175+
class TestFindStocksEdgeCases:
176+
@pytest.mark.integration
177+
def test_nonexistent_gene_id(self):
178+
stocks = find_stocks("FBgn9999999999")
179+
assert stocks == []
180+
181+
@pytest.mark.integration
182+
def test_nonexistent_stock_id(self):
183+
stocks = find_stocks("FBst9999999999")
184+
assert stocks == []
185+
186+
def test_bad_id_prefix(self):
187+
with pytest.raises(ValueError, match="Unrecognised ID prefix"):
188+
find_stocks("INVALID0001")

0 commit comments

Comments
 (0)