Skip to content

Commit 7931b30

Browse files
authored
Merge pull request #321 from BioAnalyticResource/dev
Maize Update
2 parents a320300 + d6eb732 commit 7931b30

65 files changed

Lines changed: 53604 additions & 505 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.flake8

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
[flake8]
2-
max-line-length = 120
3-
extend-ignore = E203, W503, E501
2+
ignore = E501, E203, E121, E123, E126, W503, W504
3+
per-file-ignores =
4+
# DATABASE_SPECIES uses aligned dict values for readability (intentional)
5+
api/utils/gene_id_utils.py: E241
46
exclude =
57
.git,
6-
__pycache__,
7-
docs/source/conf.py,
8-
old,
9-
build,
10-
dist,
11-
venv,
12-
env,
138
.venv,
14-
./venv-docs,
9+
__pycache__,
10+
api/Archive,
11+
data,
1512
docs,
16-
.env
13+
instance,
14+
output,
15+
venv

.github/workflows/bar-api.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jobs:
1212

1313
runs-on: ubuntu-24.04
1414
strategy:
15+
fail-fast: false
1516
matrix:
1617
python-version: [3.10.18, 3.11, 3.12, 3.13]
1718

.github/workflows/codeql.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@ jobs:
2929

3030
# Initializes the CodeQL tools for scanning.
3131
- name: Initialize CodeQL
32-
uses: github/codeql-action/init@v2
32+
uses: github/codeql-action/init@v3
3333
with:
3434
languages: ${{ matrix.language }}
3535

3636
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
3737
# If this step fails, then you should remove it and run the build manually (see below)
3838
- name: Autobuild
39-
uses: github/codeql-action/autobuild@v2
39+
uses: github/codeql-action/autobuild@v3
4040

4141
- name: Perform CodeQL Analysis
42-
uses: github/codeql-action/analyze@v2
42+
uses: github/codeql-action/analyze@v3
4343
with:
4444
category: "/language:${{matrix.language}}"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,6 @@ dmypy.json
141141
output/*
142142
!output
143143
!output/placeholder.txt
144+
145+
# Local sqlite mirrors generated from MySQL dumps
146+
config/databases/*.db

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
[![Website Status](https://img.shields.io/website?url=http%3A%2F%2Fbar.utoronto.ca%2Fapi%2F)](http://bar.utoronto.ca/api/) ![GitHub repo size](https://img.shields.io/github/repo-size/BioAnalyticResource/BAR_API) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Documentation Status](https://readthedocs.org/projects/bar-api/badge/?version=latest)](https://bar-api.readthedocs.io/en/latest/?badge=latest)
66

7-
This is the official repository for the Bio-Analytic Resource API. The API documentation can be found [here](https://bar-api.readthedocs.io/en/latest/).
7+
This is the official repository for the Bio-Analytic Resource API. The API documentation can be found [here](https://bar-api.readthedocs.io/en/latest/).

api/Archive/analyze_efp_schemas.py

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
"""
2+
Analyze eFP database structures to find the most compact schema representation.
3+
4+
Since we only need 3 columns (data_probeset_id, data_signal, data_bot_id),
5+
this script groups databases by their column signatures to identify
6+
shared patterns and enable a table-driven schema definition.
7+
8+
Usage:
9+
python api/Archive/analyze_efp_schemas.py
10+
"""
11+
12+
import csv
13+
from collections import defaultdict
14+
15+
STRUCTURE_CSV = "api/Archive/efp_tables_structure_sample_data_dump_01_28_25.csv"
16+
SAMPLE_DATA_CSV = "api/Archive/sample_data_export_feb_4.csv"
17+
18+
# Only these 3 columns matter for the API
19+
NEEDED_COLUMNS = {"data_probeset_id", "data_signal", "data_bot_id"}
20+
21+
# Extra columns that some databases have (we want to know which ones)
22+
EXTRA_COLUMNS = {
23+
"channel",
24+
"data_call",
25+
"data_num",
26+
"data_p_val",
27+
"data_p_value",
28+
"genome",
29+
"genome_id",
30+
"log",
31+
"orthogroup",
32+
"p_val",
33+
"project_id",
34+
"qvalue",
35+
"sample_file_name",
36+
"sample_tissue",
37+
"version",
38+
}
39+
40+
41+
def parse_structure_csv():
42+
"""Parse the structure CSV into per-database column definitions."""
43+
db_columns = defaultdict(dict) # {db_name: {col_name: {type, nullable, default}}}
44+
45+
with open(STRUCTURE_CSV, newline="") as f:
46+
reader = csv.DictReader(f)
47+
for row in reader:
48+
db = row["database_name"]
49+
col = row["COLUMN_NAME"]
50+
db_columns[db][col] = {
51+
"type": row["COLUMN_TYPE"],
52+
"nullable": row["IS_NULLABLE"] == "YES",
53+
"default": row["COLUMN_DEFAULT"],
54+
}
55+
return db_columns
56+
57+
58+
def extract_signature(db_cols):
59+
"""
60+
Extract a signature tuple for the 3 needed columns.
61+
Returns (probeset_type, probeset_nullable, signal_nullable, signal_default, bot_type, bot_nullable)
62+
"""
63+
p = db_cols.get("data_probeset_id", {})
64+
s = db_cols.get("data_signal", {})
65+
b = db_cols.get("data_bot_id", {})
66+
return (
67+
p.get("type", "?"),
68+
p.get("nullable", False),
69+
s.get("nullable", False),
70+
s.get("default", "NULL"),
71+
b.get("type", "?"),
72+
b.get("nullable", False),
73+
)
74+
75+
76+
def parse_varchar_length(col_type):
77+
"""Extract length from varchar(N) or return None for tinytext/text."""
78+
if col_type.startswith("varchar("):
79+
return int(col_type[8:-1])
80+
return None # tinytext, text, etc.
81+
82+
83+
def main():
84+
db_columns = parse_structure_csv()
85+
86+
print("=" * 80)
87+
print("EFP SCHEMA ANALYSIS - Only 3 columns needed")
88+
print("=" * 80)
89+
print(f"\nTotal databases: {len(db_columns)}")
90+
print(f"Needed columns: {', '.join(sorted(NEEDED_COLUMNS))}")
91+
92+
# ---- 1. Check which databases have extra columns beyond the 3 ----
93+
print("\n" + "=" * 80)
94+
print("DATABASES WITH EXTRA COLUMNS (beyond the 3 needed + proj_id + sample_id)")
95+
print("=" * 80)
96+
dbs_with_extras = {}
97+
for db, cols in sorted(db_columns.items()):
98+
extras = set(cols.keys()) - NEEDED_COLUMNS - {"proj_id", "sample_id"}
99+
if extras:
100+
dbs_with_extras[db] = extras
101+
print(f" {db}: {', '.join(sorted(extras))}")
102+
103+
dbs_simple = set(db_columns.keys()) - set(dbs_with_extras.keys())
104+
print(f"\n -> {len(dbs_simple)} databases have ONLY the 5 standard columns")
105+
print(f" -> {len(dbs_with_extras)} databases have extra columns")
106+
107+
# ---- 2. Group databases by their 3-column signature ----
108+
print("\n" + "=" * 80)
109+
print(
110+
"GROUPING BY SIGNATURE (probeset_type, probeset_nullable, signal_nullable, signal_default, bot_type, bot_nullable)"
111+
)
112+
print("=" * 80)
113+
114+
sig_groups = defaultdict(list)
115+
for db, cols in sorted(db_columns.items()):
116+
sig = extract_signature(cols)
117+
sig_groups[sig].append(db)
118+
119+
for sig, dbs in sorted(sig_groups.items(), key=lambda x: -len(x[1])):
120+
print(
121+
f"\n Signature: probeset={sig[0]}(nullable={sig[1]}) signal(nullable={sig[2]}, default={sig[3]}) bot={sig[4]}(nullable={sig[5]})"
122+
)
123+
print(f" Count: {len(dbs)}")
124+
print(f" DBs: {', '.join(dbs[:10])}{'...' if len(dbs) > 10 else ''}")
125+
126+
# ---- 3. Group by (probeset_len, bot_len) - the key variable dimensions ----
127+
print("\n" + "=" * 80)
128+
print("DATA-DRIVEN COMPACT FORMAT: Group by (probeset_type, bot_type)")
129+
print("Only considering the 3 needed columns")
130+
print("=" * 80)
131+
132+
# For the compact representation, what varies per database is:
133+
# - data_probeset_id: type (varchar(N) or tinytext) and length
134+
# - data_bot_id: type (varchar(N) or tinytext) and length
135+
# - data_signal: nullable and default (always float)
136+
# We can represent this as a tuple per database
137+
138+
compact_entries = []
139+
for db, cols in sorted(db_columns.items()):
140+
p = cols.get("data_probeset_id", {})
141+
s = cols.get("data_signal", {})
142+
b = cols.get("data_bot_id", {})
143+
144+
probeset_type = p.get("type", "varchar(24)")
145+
bot_type = b.get("type", "varchar(16)")
146+
signal_nullable = s.get("nullable", False)
147+
148+
probeset_len = parse_varchar_length(probeset_type)
149+
bot_len = parse_varchar_length(bot_type)
150+
151+
# Determine extra columns this DB needs
152+
extras = set(cols.keys()) - NEEDED_COLUMNS - {"proj_id", "sample_id"}
153+
154+
compact_entries.append(
155+
{
156+
"db": db,
157+
"probeset_len": probeset_len, # None = tinytext
158+
"probeset_type": probeset_type,
159+
"bot_len": bot_len, # None = tinytext
160+
"bot_type": bot_type,
161+
"signal_nullable": signal_nullable,
162+
"extras": extras,
163+
}
164+
)
165+
166+
# ---- 4. Show the most compact table-driven representation ----
167+
print("\n" + "=" * 80)
168+
print("PROPOSED COMPACT TUPLE FORMAT")
169+
print("Each DB needs: (name, probeset_len_or_None, bot_len_or_None, signal_nullable)")
170+
print("None = tinytext (TEXT in our schema)")
171+
print("=" * 80)
172+
173+
# Group by shared properties to find patterns
174+
pattern_groups = defaultdict(list)
175+
for e in compact_entries:
176+
key = (e["probeset_len"], e["bot_len"], e["signal_nullable"], tuple(sorted(e["extras"])))
177+
pattern_groups[key].append(e["db"])
178+
179+
print(f"\nUnique (probeset_len, bot_len, signal_nullable, extras) combinations: {len(pattern_groups)}")
180+
print("\nTop patterns (most databases sharing the same column spec):")
181+
for (pl, bl, sn, ex), dbs in sorted(pattern_groups.items(), key=lambda x: -len(x[1]))[:20]:
182+
extras_str = f", extras={list(ex)}" if ex else ""
183+
print(f" probeset={pl}, bot={bl}, signal_nullable={sn}{extras_str}")
184+
print(f" Count: {len(dbs)}, DBs: {', '.join(dbs[:5])}{'...' if len(dbs) > 5 else ''}")
185+
186+
# ---- 5. Generate the most compact code ----
187+
print("\n" + "=" * 80)
188+
print("GENERATED COMPACT TABLE (for efp_schemas.py)")
189+
print("Format: (db_name, probeset_len, bot_len)")
190+
print(" - probeset_len: int for varchar(N), 0 for tinytext")
191+
print(" - bot_len: int for varchar(N), 0 for tinytext")
192+
print(" - signal is always float, nullable is always True (safe default)")
193+
print("=" * 80)
194+
195+
# Simple databases (only 3 needed columns, no extras of concern)
196+
simple_dbs = []
197+
complex_dbs = []
198+
for e in compact_entries:
199+
# Filter out databases that ONLY have unneeded extras
200+
# (sample_file_name, data_call, data_p_val etc. are not needed)
201+
has_important_extras = e["extras"] - {"sample_file_name", "data_call", "data_p_val", "data_p_value", "data_num"}
202+
if has_important_extras:
203+
complex_dbs.append(e)
204+
else:
205+
simple_dbs.append(e)
206+
207+
print(f"\nSimple databases (only need 3 columns): {len(simple_dbs)}")
208+
print(f"Complex databases (have unique extra columns): {len(complex_dbs)}")
209+
210+
print("\n# ---- SIMPLE DATABASES (table-driven) ----")
211+
print("# (db_name, probeset_len, bot_len)")
212+
print("# probeset_len/bot_len: positive int = varchar(N), 0 = tinytext")
213+
print("_SIMPLE_EFP_SPECS = [")
214+
for e in sorted(simple_dbs, key=lambda x: x["db"]):
215+
pl = e["probeset_len"] if e["probeset_len"] is not None else 0
216+
bl = e["bot_len"] if e["bot_len"] is not None else 0
217+
print(f' ("{e["db"]}", {pl}, {bl}),')
218+
print("]")
219+
220+
print(f"\n# ---- COMPLEX DATABASES (need manual definition) ----")
221+
for e in sorted(complex_dbs, key=lambda x: x["db"]):
222+
pl = e["probeset_len"] if e["probeset_len"] is not None else "tinytext"
223+
bl = e["bot_len"] if e["bot_len"] is not None else "tinytext"
224+
print(f'# {e["db"]}: probeset={pl}, bot={bl}, extras={sorted(e["extras"])}')
225+
226+
# ---- 6. Analyze sample data for testing ----
227+
print("\n" + "=" * 80)
228+
print("SAMPLE DATA SUMMARY (for test verification)")
229+
print("=" * 80)
230+
231+
try:
232+
db_samples = defaultdict(list)
233+
with open(SAMPLE_DATA_CSV, newline="") as f:
234+
reader = csv.DictReader(f)
235+
for row in reader:
236+
db_samples[row["source_database"]].append(
237+
{
238+
"data_bot_id": row["data_bot_id"],
239+
"data_probeset_id": row["data_probeset_id"],
240+
"data_signal": row["data_signal"],
241+
}
242+
)
243+
244+
print(f"Total databases with sample data: {len(db_samples)}")
245+
print(f"Total sample rows: {sum(len(v) for v in db_samples.values())}")
246+
247+
# Verify sample data matches structure
248+
for db in sorted(db_samples.keys()):
249+
if db not in db_columns:
250+
print(f" WARNING: {db} has sample data but no structure definition!")
251+
for db in sorted(db_columns.keys()):
252+
if db not in db_samples:
253+
print(f" WARNING: {db} has structure but no sample data!")
254+
255+
except FileNotFoundError:
256+
print(" Sample data file not found, skipping.")
257+
258+
# ---- 7. Final recommendation ----
259+
print("\n" + "=" * 80)
260+
print("RECOMMENDATION")
261+
print("=" * 80)
262+
print(f"""
263+
Since you only need 3 columns (data_probeset_id, data_signal, data_bot_id),
264+
the entire schema can be reduced to a simple lookup table.
265+
266+
Current efp_schemas.py: ~1984 lines
267+
Proposed compact version: ~{len(simple_dbs) + 50} lines (table + builder)
268+
269+
Each database only differs in:
270+
1. data_probeset_id length (varchar(N) or tinytext)
271+
2. data_bot_id length (varchar(N) or tinytext)
272+
273+
data_signal is always float.
274+
275+
The compact format uses a list of tuples:
276+
(db_name, probeset_len, bot_len)
277+
278+
A single builder function converts these tuples into full schema dicts.
279+
280+
Complex databases ({len(complex_dbs)}) that have unique extra columns
281+
(channel, genome, genome_id, orthogroup, version, log, p_val, qvalue,
282+
sample_tissue) need individual definitions.
283+
""")
284+
285+
286+
if __name__ == "__main__":
287+
main()

0 commit comments

Comments
 (0)