Skip to content

Commit f039563

Browse files
authored
Merge pull request #298 from BioAnalyticResource/dev
Updated main branch
2 parents 5aac55a + a1e1bd7 commit f039563

File tree

6 files changed

+288
-36
lines changed

6 files changed

+288
-36
lines changed

api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def create_app():
5252

5353
# Now add routes
5454
from api.resources.gene_information import gene_information
55+
from api.resources.gaia import gaia
5556
from api.resources.rnaseq_gene_expression import rnaseq_gene_expression
5657
from api.resources.microarray_gene_expression import microarray_gene_expression
5758
from api.resources.proxy import bar_proxy
@@ -66,6 +67,7 @@ def create_app():
6667
from api.resources.llama3 import llama3
6768

6869
bar_api.add_namespace(gene_information)
70+
bar_api.add_namespace(gaia)
6971
bar_api.add_namespace(rnaseq_gene_expression)
7072
bar_api.add_namespace(microarray_gene_expression)
7173
bar_api.add_namespace(bar_proxy)

api/models/gaia.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from typing import List
2+
from sqlalchemy import ForeignKey
3+
from sqlalchemy.orm import relationship
4+
from api import db
5+
6+
7+
class Genes(db.Model):
8+
__bind_key__ = "gaia"
9+
__tablename__ = "genes"
10+
11+
id: db.Mapped[int] = db.mapped_column(db.Integer, nullable=False, primary_key=True)
12+
species: db.Mapped[str] = db.mapped_column(db.String(64), nullable=False)
13+
locus: db.Mapped[str] = db.mapped_column(db.String(64), nullable=True)
14+
geneid: db.Mapped[str] = db.mapped_column(db.String(32), nullable=True)
15+
children: db.Mapped[List["Aliases"]] = relationship()
16+
17+
18+
class Aliases(db.Model):
19+
__bind_key__ = "gaia"
20+
__tablename__ = "aliases"
21+
22+
id: db.Mapped[int] = db.mapped_column(db.Integer, nullable=False, primary_key=True)
23+
genes_id: db.Mapped[int] = db.mapped_column(ForeignKey("genes.id", ondelete="CASCADE"), nullable=False)
24+
alias: db.Mapped[str] = db.mapped_column(db.String(256), nullable=False)
25+
26+
27+
class PublicationFigures(db.Model):
28+
__bind_key__ = "gaia"
29+
__tablename__ = "publication_figures"
30+
31+
id: db.Mapped[int] = db.mapped_column(db.Integer, nullable=False, primary_key=True)
32+
title: db.Mapped[str] = db.mapped_column(db.String(512), nullable=True)
33+
abstract: db.Mapped[str] = db.mapped_column(db.Text, nullable=True)
34+
children: db.Mapped[List["PubIds"]] = relationship()
35+
children: db.Mapped[List["Figures"]] = relationship()
36+
37+
38+
class PubIds(db.Model):
39+
__bind_key__ = "gaia"
40+
__tablename__ = "pub_ids"
41+
42+
id: db.Mapped[int] = db.mapped_column(db.Integer, nullable=False, primary_key=True)
43+
publication_figures_id: db.Mapped[int] = db.mapped_column(db.Integer, nullable=False)
44+
publication_figures_id: db.Mapped[int] = db.mapped_column(
45+
ForeignKey("publication_figures.id", ondelete="CASCADE"), nullable=False
46+
)
47+
pubmed: db.Mapped[str] = db.mapped_column(db.String(16), nullable=True)
48+
pmc: db.Mapped[str] = db.mapped_column(db.String(16), nullable=True)
49+
50+
51+
class Figures(db.Model):
52+
__bind_key__ = "gaia"
53+
__tablename__ = "figures"
54+
55+
id: db.Mapped[int] = db.mapped_column(db.Integer, nullable=False, primary_key=True)
56+
publication_figures_id: db.Mapped[int] = db.mapped_column(db.Integer, nullable=False)
57+
publication_figures_id: db.Mapped[int] = db.mapped_column(
58+
ForeignKey("publication_figures.id", ondelete="CASCADE"), nullable=False
59+
)
60+
img_name: db.Mapped[str] = db.mapped_column(db.String(64), nullable=False)
61+
caption: db.Mapped[str] = db.mapped_column(db.Text, nullable=True)
62+
img_url: db.Mapped[str] = db.mapped_column(db.String(256), nullable=True)

api/resources/gaia.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
from flask import request
2+
from flask_restx import Namespace, Resource, fields
3+
from markupsafe import escape
4+
from api import db
5+
from api.utils.bar_utils import BARUtils
6+
from api.models.gaia import Genes, Aliases, PubIds, Figures
7+
from sqlalchemy import func, or_
8+
from marshmallow import Schema, ValidationError, fields as marshmallow_fields
9+
import json
10+
11+
gaia = Namespace("Gaia", description="Gaia", path="/gaia")
12+
13+
parser = gaia.parser()
14+
parser.add_argument(
15+
"terms",
16+
type=list,
17+
action="append",
18+
required=True,
19+
help="Publication IDs",
20+
default=["32492426", "32550561"],
21+
)
22+
23+
publication_request_fields = gaia.model(
24+
"Publications",
25+
{
26+
"pubmeds": fields.List(
27+
required=True,
28+
example=["32492426", "32550561"],
29+
cls_or_instance=fields.String,
30+
),
31+
},
32+
)
33+
34+
35+
# Validation is done in a different way to keep things simple
36+
class PublicationSchema(Schema):
37+
pubmeds = marshmallow_fields.List(cls_or_instance=marshmallow_fields.String)
38+
39+
40+
@gaia.route("/aliases/<string:identifier>")
41+
class GaiaAliases(Resource):
42+
@gaia.param("identifier", _in="path", default="ABI3")
43+
def get(self, identifier=""):
44+
45+
# Escape input
46+
identifier = escape(identifier)
47+
48+
# Is it valid
49+
if BARUtils.is_gaia_alias(identifier):
50+
query_ids = []
51+
data = []
52+
53+
# Check if alias exists
54+
# Note: This check can be done in on query, but optimizer is not using indexes for some reason
55+
query = db.select(Aliases.genes_id, Aliases.alias).filter(Aliases.alias == identifier)
56+
rows = db.session.execute(query).fetchall()
57+
58+
if rows and len(rows) > 0:
59+
# Alias exists. Get the genes_ids
60+
for row in rows:
61+
query_ids.append(row.genes_id)
62+
63+
else:
64+
# Alias doesn't exist. Get the ids if it's locus or ncbi id
65+
query = db.select(Genes.id).filter(or_(Genes.locus == identifier, Genes.geneid == identifier))
66+
rows = db.session.execute(query).fetchall()
67+
68+
if rows and len(rows) > 0:
69+
for row in rows:
70+
query_ids.append(row.id)
71+
else:
72+
return BARUtils.error_exit("Nothing found"), 404
73+
74+
# Left join is important in case aliases do not exist for the given locus / geneid
75+
query = (
76+
db.select(Genes.species, Genes.locus, Genes.geneid, func.json_arrayagg(Aliases.alias).label("aliases"))
77+
.select_from(Genes)
78+
.outerjoin(Aliases, Aliases.genes_id == Genes.id)
79+
.filter(Genes.id.in_(query_ids))
80+
.group_by(Genes.species, Genes.locus, Genes.geneid)
81+
)
82+
83+
rows = db.session.execute(query).fetchall()
84+
85+
if rows and len(rows) > 0:
86+
for row in rows:
87+
88+
# JSONify aliases
89+
if row.aliases:
90+
aliases = json.loads(row.aliases)
91+
else:
92+
aliases = []
93+
94+
record = {
95+
"species": row.species,
96+
"locus": row.locus,
97+
"geneid": row.geneid,
98+
"aliases": aliases,
99+
}
100+
101+
# Add the record to data
102+
data.append(record)
103+
104+
# Return final data
105+
return BARUtils.success_exit(data)
106+
107+
else:
108+
return BARUtils.error_exit("Invalid identifier"), 400
109+
110+
111+
@gaia.route("/publication_figures")
112+
class GaiaPublicationFigures(Resource):
113+
@gaia.expect(publication_request_fields)
114+
def post(self):
115+
json_data = request.get_json()
116+
117+
# Validate json
118+
try:
119+
json_data = PublicationSchema().load(json_data)
120+
except ValidationError as err:
121+
return BARUtils.error_exit(err.messages), 400
122+
123+
pubmeds = json_data["pubmeds"]
124+
125+
# Check if pubmed ids are valid
126+
for pubmed in pubmeds:
127+
if not BARUtils.is_integer(pubmed):
128+
return BARUtils.error_exit("Invalid Pubmed ID"), 400
129+
130+
# It is valid. Continue
131+
data = []
132+
133+
# Left join is important in case aliases do not exist for the given locus / geneid
134+
query = (
135+
db.select(Figures.img_name, Figures.caption, Figures.img_url, PubIds.pubmed, PubIds.pmc)
136+
.select_from(Figures)
137+
.join(PubIds, PubIds.publication_figures_id == Figures.publication_figures_id)
138+
.filter(PubIds.pubmed.in_(pubmeds))
139+
.order_by(PubIds.pubmed.desc())
140+
)
141+
142+
rows = db.session.execute(query).fetchall()
143+
144+
record = {}
145+
146+
if rows and len(rows) > 0:
147+
for row in rows:
148+
149+
# Check if record has an id. If it doesn't, this is first row.
150+
if "id" in record:
151+
# Check if this is a new pubmed id
152+
if record["id"]["pubmed"] != row.pubmed:
153+
# new record. Add old now to data and create a new record
154+
data.append(record)
155+
record = {}
156+
157+
# Check if figures exists, if not add it.
158+
if record.get("figures") is None:
159+
# Create a new figures record
160+
record["figures"] = []
161+
162+
# Now append figure to the record
163+
figure = {"img_name": row.img_name, "caption": row.caption, "img_url": row.img_url}
164+
record["figures"].append(figure)
165+
166+
# Now add the id. If it exists don't add
167+
if record.get("id") is None:
168+
record["id"] = {}
169+
record["id"]["pubmed"] = row.pubmed
170+
record["id"]["pmc"] = row.pmc
171+
172+
# The last record
173+
data.append(record)
174+
175+
# Return final data
176+
return BARUtils.success_exit(data)

api/utils/bar_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,17 @@ def is_integer(data):
252252
else:
253253
return False
254254

255+
@staticmethod
256+
def is_gaia_alias(data):
257+
"""Check if the input is a valid gaia alias.
258+
:param data
259+
:return: True if valid gaia alias
260+
"""
261+
if re.search(r"^[a-z0-9_]{1,50}$", data, re.I):
262+
return True
263+
else:
264+
return False
265+
255266
@staticmethod
256267
def format_poplar(poplar_gene):
257268
"""Format Poplar gene ID to be Potri.016G107900, i.e. capitalized P and G

docs/requirements.txt

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
accessible-pygments==0.0.5
22
alabaster==1.0.0
33
babel==2.17.0
4-
beautifulsoup4==4.13.4
5-
certifi==2025.7.14
6-
charset-normalizer==3.4.2
7-
docutils==0.21.2
8-
furo==2025.7.19
9-
idna==3.10
4+
beautifulsoup4==4.14.3
5+
certifi==2026.1.4
6+
charset-normalizer==3.4.4
7+
docutils==0.22.4
8+
furo==2025.12.19
9+
idna==3.11
1010
imagesize==1.4.1
1111
Jinja2==3.1.6
12-
MarkupSafe==3.0.2
13-
packaging==25.0
12+
MarkupSafe==3.0.3
13+
packaging==26.0
1414
Pygments==2.19.2
1515
pytz==2025.2
16-
requests==2.32.4
16+
requests==2.32.5
17+
roman-numerals==4.1.0
1718
roman-numerals-py==3.1.0
18-
setuptools==80.9.0
19+
setuptools==80.10.1
1920
snowballstemmer==3.0.1
20-
soupsieve==2.7
21-
Sphinx==8.2.3
21+
soupsieve==2.8.3
22+
Sphinx==9.1.0
2223
sphinx-basic-ng==1.0.0b2
2324
sphinx-copybutton==0.5.2
2425
sphinxcontrib-applehelp==2.0.0
@@ -27,6 +28,6 @@ sphinxcontrib-htmlhelp==2.1.0
2728
sphinxcontrib-jsmath==1.0.1
2829
sphinxcontrib-qthelp==2.0.0
2930
sphinxcontrib-serializinghtml==2.0.0
30-
typing_extensions==4.14.1
31-
urllib3==2.5.0
31+
typing_extensions==4.15.0
32+
urllib3==2.6.3
3233
wheel==0.45.1

0 commit comments

Comments
 (0)