scigantic-bindingdb 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {scigantic_bindingdb-0.2.2/src/scigantic_bindingdb.egg-info → scigantic_bindingdb-0.2.3}/PKG-INFO +1 -1
  2. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/pyproject.toml +1 -1
  3. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb/chembl_bridge.py +12 -6
  4. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb/dti_pairs.py +7 -2
  5. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3/src/scigantic_bindingdb.egg-info}/PKG-INFO +1 -1
  6. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/tests/test_chembl_bridge.py +24 -0
  7. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/tests/test_dti_pairs.py +24 -0
  8. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/LICENSE +0 -0
  9. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/README.md +0 -0
  10. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/setup.cfg +0 -0
  11. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb/__init__.py +0 -0
  12. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb/__main__.py +0 -0
  13. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb/_constants.py +0 -0
  14. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb/cache.py +0 -0
  15. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb/cli.py +0 -0
  16. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb/connection.py +0 -0
  17. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb/measurements.py +0 -0
  18. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb/py.typed +0 -0
  19. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb/releases.py +0 -0
  20. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb.egg-info/SOURCES.txt +0 -0
  21. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb.egg-info/dependency_links.txt +0 -0
  22. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb.egg-info/entry_points.txt +0 -0
  23. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb.egg-info/requires.txt +0 -0
  24. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/src/scigantic_bindingdb.egg-info/top_level.txt +0 -0
  25. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/tests/test_cache.py +0 -0
  26. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/tests/test_cli.py +0 -0
  27. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/tests/test_connection.py +0 -0
  28. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/tests/test_measurements.py +0 -0
  29. {scigantic_bindingdb-0.2.2 → scigantic_bindingdb-0.2.3}/tests/test_releases.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scigantic-bindingdb
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Query BindingDB directly from a public S3 mirror with DuckDB, including a ChEMBL cross-reference bridge table and a ready drug-target-interaction training table.
5
5
  Author: Scigantic
6
6
  License: MIT-0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scigantic-bindingdb"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Query BindingDB directly from a public S3 mirror with DuckDB, including a ChEMBL cross-reference bridge table and a ready drug-target-interaction training table."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -49,10 +49,6 @@ def chembl_bridge(
49
49
  con = connect(release)
50
50
  try:
51
51
  bridge_path = _resolve(f"{release}/derived/bindingdb_chembl_bridge.parquet")
52
- con.execute(
53
- "CREATE OR REPLACE VIEW chembl_bridge AS "
54
- f"SELECT * FROM read_parquet('{bridge_path}')"
55
- )
56
52
 
57
53
  where: list[str] = []
58
54
  params: list[str | int] = []
@@ -61,12 +57,22 @@ def chembl_bridge(
61
57
  params.append(reactant_set_id)
62
58
  clause = f"WHERE {' AND '.join(where)}" if where else ""
63
59
 
60
+ # read_parquet(bridge_path) inline rather than a named
61
+ # CREATE VIEW: connect() now hands out a cursor on a base
62
+ # connection shared across calls (see connection.py), and a
63
+ # named view is catalog state on that shared connection.
64
+ # Concurrent calls each doing CREATE OR REPLACE VIEW on the same
65
+ # name raced each other's DDL transaction: reproduced directly,
66
+ # concurrent chembl_bridge()/dti_pairs() calls from a thread pool
67
+ # mostly failed with DuckDB's "Catalog write-write conflict".
68
+ # Referencing the parquet file directly in the query has no
69
+ # catalog side effect at all, so there's nothing to conflict on.
64
70
  if with_names:
65
71
  mol_dict = f"s3://{CHEMBL_BUCKET}/{chembl_release}/parquet/molecule_dictionary.parquet"
66
72
  sql = f"""
67
73
  SELECT b.reactant_set_id, b.chembl_molregno, b.chembl_id, b.match_method,
68
74
  m.ligand_smiles, d.pref_name AS chembl_pref_name
69
- FROM chembl_bridge b
75
+ FROM read_parquet('{bridge_path}') b
70
76
  JOIN measurements m ON m.reactant_set_id = b.reactant_set_id
71
77
  LEFT JOIN read_parquet('{mol_dict}') d ON d.molregno = b.chembl_molregno
72
78
  {clause}
@@ -75,7 +81,7 @@ def chembl_bridge(
75
81
  sql = f"""
76
82
  SELECT b.reactant_set_id, b.chembl_molregno, b.chembl_id, b.match_method,
77
83
  m.ligand_smiles
78
- FROM chembl_bridge b
84
+ FROM read_parquet('{bridge_path}') b
79
85
  JOIN measurements m ON m.reactant_set_id = b.reactant_set_id
80
86
  {clause}
81
87
  """
@@ -54,7 +54,6 @@ def dti_pairs(
54
54
  con = connect(release)
55
55
  try:
56
56
  path = _resolve(f"{release}/derived/dti_pairs.parquet")
57
- con.execute(f"CREATE OR REPLACE VIEW dti_pairs AS SELECT * FROM read_parquet('{path}')")
58
57
 
59
58
  where: list[str] = []
60
59
  params: list[str | int] = []
@@ -68,7 +67,13 @@ def dti_pairs(
68
67
  where.append("n_chains_declared = 1")
69
68
  clause = f"WHERE {' AND '.join(where)}" if where else ""
70
69
 
71
- sql = f"SELECT * FROM dti_pairs {clause} ORDER BY p_affinity DESC"
70
+ # read_parquet(path) inline rather than a named CREATE VIEW: see
71
+ # chembl_bridge.py's comment on the same change. connect() now
72
+ # returns a cursor on a base connection shared across calls, and a
73
+ # named view is catalog state on that shared connection --
74
+ # concurrent calls racing CREATE OR REPLACE VIEW on the same name
75
+ # hit DuckDB's "Catalog write-write conflict", reproduced directly.
76
+ sql = f"SELECT * FROM read_parquet('{path}') {clause} ORDER BY p_affinity DESC"
72
77
  if limit is not None:
73
78
  sql += " LIMIT ?"
74
79
  params.append(int(limit))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scigantic-bindingdb
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Query BindingDB directly from a public S3 mirror with DuckDB, including a ChEMBL cross-reference bridge table and a ready drug-target-interaction training table.
5
5
  Author: Scigantic
6
6
  License: MIT-0
@@ -1,3 +1,5 @@
1
+ from concurrent.futures import ThreadPoolExecutor
2
+
1
3
  import scigantic_bindingdb as bindingdb
2
4
 
3
5
  # A gefitinib measurement, verified against the live mirror to resolve
@@ -29,3 +31,25 @@ def test_bridge_total_row_count():
29
31
  "'s3://scigantic-bindingdb/202608/derived/bindingdb_chembl_bridge.parquet')"
30
32
  )
31
33
  assert total["n"].iloc[0] == 2272063
34
+
35
+
36
+ def test_concurrent_calls_do_not_conflict_on_catalog():
37
+ # Regression test for a real bug introduced by connect()'s move to a
38
+ # shared base connection (see connection.py): chembl_bridge() used to
39
+ # register its result as a named CREATE OR REPLACE VIEW, which is
40
+ # catalog-mutating DDL. Two calls racing that DDL on the same shared
41
+ # connection from different threads mostly failed with DuckDB's
42
+ # "Catalog write-write conflict", reproduced directly (46/48 calls
43
+ # failed in a 24-thread stress run) before this was fixed by
44
+ # referencing the parquet file inline instead of via a named view.
45
+ def call(i):
46
+ return bindingdb.chembl_bridge(
47
+ reactant_set_id=_GEFITINIB_REACTANT_SET_ID, with_names=(i % 2 == 0)
48
+ )
49
+
50
+ with ThreadPoolExecutor(max_workers=16) as pool:
51
+ results = list(pool.map(call, range(16)))
52
+
53
+ for df in results:
54
+ assert len(df) == 1
55
+ assert df["chembl_id"].iloc[0] == "CHEMBL939"
@@ -1,4 +1,5 @@
1
1
  import math
2
+ from concurrent.futures import ThreadPoolExecutor
2
3
 
3
4
  import scigantic_bindingdb as bindingdb
4
5
 
@@ -69,3 +70,26 @@ def test_uniprot_filter():
69
70
  def test_sorted_most_potent_first():
70
71
  df = bindingdb.dti_pairs(endpoint="ki", uniprot_id="P00533")
71
72
  assert df["p_affinity"].is_monotonic_decreasing
73
+
74
+
75
+ def test_concurrent_calls_do_not_conflict_on_catalog():
76
+ # Regression test for a real bug introduced by connect()'s move to a
77
+ # shared base connection (see connection.py): dti_pairs() used to
78
+ # register its result as a named CREATE OR REPLACE VIEW, which is
79
+ # catalog-mutating DDL. Two calls racing that DDL on the same shared
80
+ # connection from different threads mostly failed with DuckDB's
81
+ # "Catalog write-write conflict" (reproduced directly before this was
82
+ # fixed by referencing the parquet file inline instead of via a named
83
+ # view; see chembl_bridge.py's equivalent test).
84
+ endpoints = ("ki", "ic50", "kd", "ec50")
85
+
86
+ def call(i):
87
+ endpoint = endpoints[i % len(endpoints)]
88
+ return endpoint, bindingdb.dti_pairs(endpoint=endpoint, limit=50)
89
+
90
+ with ThreadPoolExecutor(max_workers=16) as pool:
91
+ results = list(pool.map(call, range(16)))
92
+
93
+ for endpoint, df in results:
94
+ assert len(df) == 50
95
+ assert (df["endpoint"] == endpoint).all()