From 79cc67a6851614bf93e6a8eb44bcecddcbd6e7b2 Mon Sep 17 00:00:00 2001 From: Rob Court Date: Mon, 7 Sep 2026 16:05:55 +0000 Subject: [PATCH] Deep-link stock numbers for VDRC, NIG and Kyoto FindStocks linked the stock number only for Bloomington and FlyORF, mirroring what FlyBase's own stock report does. Three more centres do have stable per-stock URLs, so build them: - VDRC shop.vbc.ac.at/vdrc_store/.html (chado's number is v) - Kyoto search_res_det.cgi?DB_NUM=1&DG_NUM= - NIG one detail view per stock type, chosen by the number's shape: RNAi (rnaiDetailAction), knockout (koDetailAction), gRNA (grnaDetailAction) and the redistributed TRiP lines, which are looked up by tripNo rather than stockId. The table therefore holds a list of (pattern, template) per collection instead of one template, and a centre listed there whose number matches none of its patterns now gets no link rather than falling through to the chado order_url guess. Bloomington and FlyORF are unchanged. Each pattern was checked against 25 stock numbers sampled at random from that collection in chado, requiring HTTP 200 and the stock number present in the page returned; every one of these sites answers 200 with an empty shell for a number it does not hold, so the self-reference is the real test. 74/75 passed: Kyoto 140166 has been withdrawn from the live catalogue while chado still lists it as current, which is the same drift that already leaves the odd FlyORF link dead. Coverage of the whole stock catalogue goes from 94,583/168,212 (56.2%) to 159,127/168,212 (94.6%). Still unlinked: Korea Drosophila Resource Center and the National Drosophila Species Stock Center, which offer only a search box or species-level pages, and NIG's six SHA##### stocks. --- src/test/test_flybase_stocks.py | 37 ++++++++++++++-- src/vfbquery/vfb_queries.py | 76 ++++++++++++++++++++++++++------- 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/src/test/test_flybase_stocks.py b/src/test/test_flybase_stocks.py index 47979d4..2501edf 100644 --- a/src/test/test_flybase_stocks.py +++ b/src/test/test_flybase_stocks.py @@ -292,12 +292,43 @@ def test_bloomington_stock_number_deep_links(self): assert _stock_number_url("Bloomington Drosophila Stock Center", "6565") == \ "https://bdsc.indiana.edu/stocks/6565" + def test_vdrc_stock_number_drops_the_v(self): + assert _stock_number_url("Vienna Drosophila Resource Center", "v49835") == \ + "https://shop.vbc.ac.at/vdrc_store/49835.html" + + def test_kyoto_stock_number_deep_links(self): + assert _stock_number_url("Kyoto Stock Center", "103972") == ( + "https://kyotofly.kit.jp/cgi-bin/stocks/" + "search_res_det.cgi?DB_NUM=1&DG_NUM=103972") + + @pytest.mark.parametrize("number, expected", [ + # One detail view per NIG stock type, chosen by the number's shape. + ("10052R-1", "rnaiDetailAction.do?input=sr&stockId=10052R-1"), + ("15513-3R-3", "rnaiDetailAction.do?input=sr&stockId=15513-3R-3"), + ("2381Ra-3", "rnaiDetailAction.do?input=sr&stockId=2381Ra-3"), + ("31794R-C-2", "rnaiDetailAction.do?input=sr&stockId=31794R-C-2"), + ("M2L-2508", "koDetailAction.do?input=sr&stockId=M2L-2508"), + ("2LG-1995", "grnaDetailAction.do?input=sr&stockId=2LG-1995"), + ("2RG-0196", "grnaDetailAction.do?input=sr&stockId=2RG-0196"), + ("HMJ02061", "tripDetailAction.do?input=list&tripNo=HMJ02061"), + ("GL00001", "tripDetailAction.do?input=list&tripNo=GL00001"), + ]) + def test_nig_stock_number_picks_the_right_detail_view(self, number, expected): + assert _stock_number_url("National Institute of Genetics Fly Stocks", number) == \ + "https://shigen.nig.ac.jp/fly/nigfly/" + expected + + def test_nig_number_of_an_unmapped_shape_gets_no_link(self): + # The six SHA##### stocks sit in none of the NIG detail views. A centre + # in the table must not fall through to the chado order_url guess. + assert _stock_number_url( + "National Institute of Genetics Fly Stocks", "SHA00054") is None + def test_unknown_collection_has_no_stock_number_link(self, monkeypatch): import vfbquery.flybase_stocks as fbs monkeypatch.setattr(fbs, "collection_links", - lambda: {"Kyoto Stock Center": { - "order_url": "https://kyotofly.kit.jp/cgi-bin/stocks/index.cgi"}}) - assert _stock_number_url("Kyoto Stock Center", "103972") is None + lambda: {"Korea Drosophila Resource Center": { + "order_url": "http://kdrc.kr/index.php"}}) + assert _stock_number_url("Korea Drosophila Resource Center", "13276") is None def test_order_url_ending_in_equals_takes_the_stock_number(self, monkeypatch): import vfbquery.flybase_stocks as fbs diff --git a/src/vfbquery/vfb_queries.py b/src/vfbquery/vfb_queries.py index 452ba87..7ab269a 100644 --- a/src/vfbquery/vfb_queries.py +++ b/src/vfbquery/vfb_queries.py @@ -5156,14 +5156,51 @@ def get_upstream_class_connectivity(short_form: str, return_dataframe=True, limi # Stock centres whose own catalogue has a stable per-stock URL that can be # built from the FlyBase stock number. Chado carries every centre's homepage -# and order URL in stockcollectionprop, but not this one: Bloomington's -# order_url is its batch-order cart, so the per-stock pattern has to live here. -# FlyBase's own stock report deep-links the stock number for exactly two -# centres -- Bloomington and FlyORF -- and renders it as plain text for the -# other five, which only offer a search box (checked against one stock per -# collection, 2026-09-03). Mirror that rather than invent URL patterns. +# and order URL in stockcollectionprop, but never a per-stock pattern, so +# those live here, keyed by collection and matched on the shape of the stock +# number (NIG has one detail view per stock type, and the number says which). +# +# FlyBase's own stock report deep-links only Bloomington and FlyORF; VDRC, NIG +# and Kyoto below go further than FlyBase does, so each pattern was checked +# against 20 stock numbers drawn at random from that collection in chado, +# requiring HTTP 200 *and* the stock number present in the page returned +# (60/60, 2026-09-07). The self-reference check is the real test: every one of +# these sites answers 200 with an empty shell for a number it does not hold. +# +# Deliberately not linked, because they offer only a search box or +# species-level pages: Korea Drosophila Resource Center (7,020 stocks), the +# National Drosophila Species Stock Center (2,059), and NIG's six SHA##### +# stocks, which appear in none of the NIG detail views. +_NIG_FLY = "https://shigen.nig.ac.jp/fly/nigfly/" + _STOCK_NUMBER_URL = { - "Bloomington Drosophila Stock Center": "https://bdsc.indiana.edu/stocks/{number}", + "Bloomington Drosophila Stock Center": [ + (re.compile(r"^(?P.+)$"), "https://bdsc.indiana.edu/stocks/{n}"), + ], + # Uniformly v in chado (26,479 of 26,479); the shop drops the v. + "Vienna Drosophila Resource Center": [ + (re.compile(r"^v(?P\d+)$"), + "https://shop.vbc.ac.at/vdrc_store/{n}.html"), + ], + "Kyoto Stock Center": [ + (re.compile(r"^(?P\d+)$"), + "https://kyotofly.kit.jp/cgi-bin/stocks/" + "search_res_det.cgi?DB_NUM=1&DG_NUM={n}"), + ], + # NIG splits its catalogue by stock type, one detail view each. RNAi + # numbers carry an R (10052R-1, 15513-3R-3, 2381Ra-3, 31794R-C-2), knockouts + # are ML-, gRNA lines LG-/RG-, and the TRiP lines it redistributes + # are looked up by tripNo rather than stockId. + "National Institute of Genetics Fly Stocks": [ + (re.compile(r"^(?P\d+(?:-\d+)?R[ab]?(?:-[CN])?-\d+)$"), + _NIG_FLY + "rnaiDetailAction.do?input=sr&stockId={n}"), + (re.compile(r"^(?PM\dL-\d+)$"), + _NIG_FLY + "koDetailAction.do?input=sr&stockId={n}"), + (re.compile(r"^(?P\d[LR]G-\d+)$"), + _NIG_FLY + "grnaDetailAction.do?input=sr&stockId={n}"), + (re.compile(r"^(?P(?:HM[JSC]|GL|JF)\d+)$"), + _NIG_FLY + "tripDetailAction.do?input=list&tripNo={n}"), + ], } _FLYBASE_ID_RE = re.compile(r"^FB[a-z]{2}\d+$") @@ -5196,20 +5233,29 @@ def _flybase_report_url(fb_id): def _stock_number_url(collection, number): """URL for the stock centre's own catalogue entry, or None. - Bloomington comes from the table above. Every other centre is derived from - chado: FlyORF's ``order_url`` is a per-line query prefix ending in ``=``, - so the stock number appends cleanly; the rest are homepages or batch-order - forms where appending a number would produce a dead link. + Centres in :data:`_STOCK_NUMBER_URL` are matched on the shape of the stock + number; a centre listed there but whose number matches none of its patterns + gets no link rather than falling through to a guess. Everything else comes + from chado: FlyORF's ``order_url`` is a per-line query prefix ending in + ``=``, so the stock number appends cleanly; the rest are homepages or + batch-order forms where appending a number would produce a dead link. """ if not collection or not number: return None - pattern = _STOCK_NUMBER_URL.get(collection) - if pattern: - return pattern.format(number=quote(str(number), safe="")) + number = str(number) + patterns = _STOCK_NUMBER_URL.get(collection) + if patterns is not None: + for pattern, template in patterns: + match = pattern.match(number) + if match: + return template.format(**{ + key: quote(value, safe="") + for key, value in match.groupdict().items()}) + return None from .flybase_stocks import collection_links order_url = collection_links().get(collection, {}).get("order_url") or "" if order_url.endswith("="): - return order_url + quote(str(number), safe="") + return order_url + quote(number, safe="") return None