Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: species dropdown connected to backend #100

Merged
merged 13 commits into from
Dec 3, 2023
59 changes: 23 additions & 36 deletions backend/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
* Proteins Table
*/
CREATE TABLE proteins (
name text NOT NULL UNIQUE PRIMARY KEY, -- user specified name of the protein (TODO: consider having a string limit)
id serial PRIMARY KEY,
name text NOT NULL UNIQUE, -- user specified name of the protein (TODO: consider having a string limit)
length integer, -- length of amino acid sequence
mass numeric, -- mass in amu/daltons
content bytea, -- stored markdown for the protein article (TODO: consider having a limit to how big this can be)
Expand All @@ -26,20 +27,22 @@ CREATE TABLE proteins (
* Species Table
*/
CREATE TABLE species (
id text NOT NULL UNIQUE PRIMARY KEY, -- This should be the first letters in the scientific name - e.g. "gh", the prefix to the files, for simplicity.
tax_genus text NOT NULL,
tax_species text NOT NULL,
scientific_name text UNIQUE GENERATED ALWAYS AS (tax_genus || ' ' || tax_species) STORED,
content bytea
id serial PRIMARY KEY,
name text NOT NULL UNIQUE -- combined genus and species name, provided for now by the user
-- -- removed now to reduce complexity for v0
-- tax_genus text NOT NULL,
-- tax_species text NOT NULL,
-- scientific_name text UNIQUE GENERATED ALWAYS AS (tax_genus || ' ' || tax_species) STORED,
-- content bytea
);

/*
* Table: species_proteins
* Description: Join table for N:M connection between Species and Proteins
*/
CREATE TABLE species_proteins (
species_id text references species(id) ON UPDATE CASCADE ON DELETE CASCADE,
protein_id text references proteins(name) ON UPDATE CASCADE ON DELETE CASCADE,
species_id serial references species(id) ON UPDATE CASCADE ON DELETE CASCADE,
protein_id serial references proteins(id) ON UPDATE CASCADE ON DELETE CASCADE,
PRIMARY KEY (species_id, protein_id)
);

Expand Down Expand Up @@ -73,48 +76,32 @@ INSERT INTO proteins (name, length, mass, content, refs) VALUES (
/*
* Inserts example species into species table
*/
INSERT INTO species(id, tax_genus, tax_species, content) VALUES (
'Gh',
'Ganaspis',
'hookeri',
null
);

INSERT INTO species(id, tax_genus, tax_species, content) VALUES (
'Lb',
'Leptopilina',
'boulardi',
null
);
INSERT INTO species(name) VALUES ('ganaspis hookeri');
INSERT INTO species(name) VALUES ('leptopilina boulardi');
INSERT INTO species(name) VALUES ('leptopilina heterotoma');
INSERT INTO species(name) VALUES ('unknown');

INSERT INTO species(id, tax_genus, tax_species, content) VALUES (
'Lh',
'Leptopilina',
'heterotoma',
null
);

/*
/*
* Inserts connections between species and proteins
*/
*/
INSERT INTO species_proteins(species_id, protein_id) VALUES (
'Gh',
'Gh_comp271_c0_seq1'
1, -- 'ganaspis hookeri',
1 -- 'Gh_comp271_c0_seq1'
);

/*
* Inserts connections between species and proteins
*/
INSERT INTO species_proteins(species_id, protein_id) VALUES (
'Lb',
'Lb17_comp535_c2_seq1'
2, -- 'leptopilina boulardi',
2 -- 'Lb17_comp535_c2_seq1'
);

/*
* Inserts connections between species and proteins
*/
INSERT INTO species_proteins(species_id, protein_id) VALUES (
'Lh',
'Lh14_comp2336_c0_seq1'
3, -- 'leptopilina heterotoma',
3 --'Lh14_comp2336_c0_seq1'
);

4 changes: 4 additions & 0 deletions backend/src/api_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class ProteinEntry(CamelModel):
name: str
length: int
mass: float
species_name: str
content: str | None = None
refs: str | None = None

Expand All @@ -37,6 +38,7 @@ class AllEntries(CamelModel):

class UploadBody(CamelModel):
name: str
species_name: str
content: str # markdown content from user
refs: str # references used in content (bibtex form)
pdb_file_str: str
Expand All @@ -56,5 +58,7 @@ class UploadStatus(CamelModel):
class EditBody(CamelModel):
old_name: str # so we can identify the exact row we need to change
new_name: str
old_species_name: str # so we can identify the exact row we need to change
new_species_name: str
new_content: str | None = None
new_refs: str | None = None
128 changes: 95 additions & 33 deletions backend/src/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,40 +40,45 @@ def get_all_entries():
"""
with Database() as db:
try:
entries_sql = db.execute_return(
"""SELECT name, length, mass FROM proteins"""
)
query = """SELECT proteins.name, proteins.length, proteins.mass, species.name as species_name FROM species_proteins
JOIN proteins ON species_proteins.protein_id = proteins.id
JOIN species ON species_proteins.species_id = species.id;"""
entries_sql = db.execute_return(query)
log.warn(entries_sql)

# if we got a result back
if entries_sql is not None:
return [
ProteinEntry(name=name, length=length, mass=mass)
for name, length, mass in entries_sql
ProteinEntry(
name=name, length=length, mass=mass, species_name=species_name
)
for name, length, mass, species_name in entries_sql
]
except Exception as e:
log.error(e)


@app.get("/search-entries/{query:str}", response_model=list[ProteinEntry] | None)
def search_entries(query: str):
@app.get("/search-entries/{protein_name:str}", response_model=list[ProteinEntry] | None)
def search_entries(protein_name: str):
"""Gets a list of protein entries by a search string
Returns: list[ProteinEntry] if found | None if not found
"""
with Database() as db:
try:
entries_sql = db.execute_return(
"""SELECT name, length, mass FROM proteins
WHERE name ILIKE %s""",
[f"%{query}%"],
)
query = """SELECT proteins.name, proteins.length, proteins.mass, species.name as species_name FROM species_proteins
JOIN proteins ON species_proteins.protein_id = proteins.id
JOIN species ON species_proteins.species_id = species.id
WHERE proteins.name ILIKE %s;"""
entries_sql = db.execute_return(query, [f"%{protein_name}%"])
log.warn(entries_sql)

# if we got a result back
if entries_sql is not None:
return [
ProteinEntry(name=name, length=length, mass=mass)
for name, length, mass in entries_sql
ProteinEntry(
name=name, length=length, mass=mass, species_name=species_name
)
for name, length, mass, species_name in entries_sql
]
except Exception as e:
log.error(e)
Expand All @@ -86,18 +91,18 @@ def get_protein_entry(protein_name: str):
"""
with Database() as db:
try:
entry_sql = db.execute_return(
"""SELECT name, length, mass, content, refs FROM proteins
WHERE name = %s""",
[protein_name],
)
query = """SELECT proteins.name, proteins.length, proteins.mass, proteins.content, proteins.refs, species.name as species_name FROM species_proteins
JOIN proteins ON species_proteins.protein_id = proteins.id
JOIN species ON species_proteins.species_id = species.id
WHERE proteins.name = %s;"""
entry_sql = db.execute_return(query, [protein_name])
log.warn(entry_sql)

# if we got a result back
if entry_sql is not None and len(entry_sql) != 0:
# return the only entry
only_returned_entry = entry_sql[0]
name, length, mass, content, refs = only_returned_entry
name, length, mass, content, refs, species_name = only_returned_entry

# if byte arrays are present, decode them into a string
if content is not None:
Expand All @@ -106,7 +111,12 @@ def get_protein_entry(protein_name: str):
refs = bytea_to_str(refs)

return ProteinEntry(
name=name, length=length, mass=mass, content=content, refs=refs
name=name,
length=length,
mass=mass,
content=content,
refs=refs,
species_name=species_name,
)

except Exception as e:
Expand All @@ -117,17 +127,18 @@ def get_protein_entry(protein_name: str):
@app.delete("/protein-entry/{protein_name:str}", response_model=None)
def delete_protein_entry(protein_name: str):
# Todo, have a meaningful error if the delete fails
try:
with Database() as db:
with Database() as db:
# remove protein
try:
db.execute(
"""DELETE FROM proteins
WHERE name = %s""",
[protein_name],
)
# delete the file from the data/ folder
os.remove(pdb_file_name(protein_name))
except Exception as e:
log.error(e)
# delete the file from the data/ folder
os.remove(pdb_file_name(protein_name))
except Exception as e:
log.error(e)


# None return means success
Expand All @@ -148,9 +159,24 @@ def upload_protein_entry(body: UploadBody):
# write to file to data/ folder
with open(pdb_file_name(pdb.name), "w") as f:
f.write(pdb.file_contents)
except Exception:
log.warn("Failed to write to file")
return UploadError.WRITE_ERROR

# save to db
with Database() as db:
# save to db
with Database() as db:
try:
# first add the species if it doesn't exist
db.execute(
"""INSERT INTO species (name) VALUES (%s) ON CONFLICT DO NOTHING;""",
[body.species_name],
)
except Exception:
log.warn("Failed to insert into species table")
return UploadError.QUERY_ERROR

try:
# add the protein itself
db.execute(
"""INSERT INTO proteins (name, length, mass, content, refs) VALUES (%s, %s, %s, %s, %s);""",
[
Expand All @@ -161,8 +187,21 @@ def upload_protein_entry(body: UploadBody):
str_to_bytea(body.refs),
],
)
except Exception:
return UploadError.WRITE_ERROR
except Exception:
log.warn("Failed to insert into proteins table")
return UploadError.QUERY_ERROR

try:
# connect them with the intermediate table
db.execute(
"""INSERT INTO species_proteins (species_id, protein_id)
VALUES ((SELECT id FROM species WHERE name = %s),
(SELECT id FROM proteins WHERE name = %s));""",
[body.species_name, body.name],
)
except Exception:
log.warn("Failed to insert into join table")
return UploadError.QUERY_ERROR


# TODO: add more edits, now only does name and content edits
Expand All @@ -176,6 +215,7 @@ def edit_protein_entry(body: EditBody):
os.rename(pdb_file_name(body.old_name), pdb_file_name(body.new_name))

with Database() as db:
name_changed = False
if body.new_name != body.old_name:
db.execute(
"""UPDATE proteins SET name = %s WHERE name = %s""",
Expand All @@ -184,13 +224,23 @@ def edit_protein_entry(body: EditBody):
body.old_name,
],
)
name_changed = True

if body.new_species_name != body.old_species_name:
db.execute(
"""UPDATE species_proteins SET species_id = (SELECT id FROM species WHERE name = %s) WHERE protein_id = (SELECT id FROM proteins WHERE name = %s)""",
[
body.new_species_name,
body.old_name if not name_changed else body.new_name,
],
)

if body.new_content is not None:
db.execute(
"""UPDATE proteins SET content = %s WHERE name = %s""",
[
str_to_bytea(body.new_content),
body.old_name,
body.old_name if not name_changed else body.new_name,
],
)

Expand All @@ -199,14 +249,26 @@ def edit_protein_entry(body: EditBody):
"""UPDATE proteins SET refs = %s WHERE name = %s""",
[
str_to_bytea(body.new_refs),
body.old_name,
body.old_name if not name_changed else body.new_name,
],
)

except Exception:
return UploadError.WRITE_ERROR


@app.get("/all-species", response_model=list[str] | None)
def get_all_species():
try:
with Database() as db:
query = """SELECT name as species_name FROM species"""
entry_sql = db.execute_return(query)
if entry_sql is not None:
return [d[0] for d in entry_sql]
except Exception:
return


def export_app_for_docker():
"""Needed for the [docker-compose.yml](../../docker-compose.yml)
Example: `uvicorn src.server:export_app_for_docker --reload --host 0.0.0.0`
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/ListProteins.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<Table>
<TableHead>
<TableHeadCell>Protein name</TableHeadCell>
<TableHeadCell>Organism</TableHeadCell>
<TableHeadCell>Length</TableHeadCell>
<TableHeadCell>Mass (Da)</TableHeadCell>
</TableHead>
Expand All @@ -37,6 +38,7 @@
><span class="text-primary-700">{entry.name}</span
></TableBodyCell
>
<TableBodyCell>{entry.speciesName}</TableBodyCell>
<TableBodyCell>{entry.length}</TableBodyCell>
<TableBodyCell>{numberWithCommas(entry.mass)}</TableBodyCell>
</TableBodyRow>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/openapi/models/EditBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
export type EditBody = {
oldName: string;
newName: string;
oldSpeciesName: string;
newSpeciesName: string;
newContent?: (string | null);
newRefs?: (string | null);
};

1 change: 1 addition & 0 deletions frontend/src/openapi/models/HTTPValidationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ import type { ValidationError } from './ValidationError';
export type HTTPValidationError = {
detail?: Array<ValidationError>;
};

Loading