Spaces:
Sleeping
Sleeping
Jonas
commited on
Commit
·
dd19932
1
Parent(s):
344a2d8
Add initial implementation of ADE Explorer with requirements, plotting, and API client
Browse files- README.md +49 -1
- requirements.txt +6 -0
- src/app.py +113 -0
- src/openfda_client.py +103 -0
- src/plotting.py +47 -0
- tests/test_openfda_client.py +77 -0
README.md
CHANGED
|
@@ -1 +1,49 @@
|
|
| 1 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Medication Adverse-Event Explorer (ADE Explorer)
|
| 2 |
+
|
| 3 |
+
A Gradio app that exposes a drug-event query tool as an MCP server. It has two core workflows:
|
| 4 |
+
(A) finding top adverse events for a drug, and
|
| 5 |
+
(B) checking the frequency of a specific drug-event pair.
|
| 6 |
+
|
| 7 |
+
It uses public data from OpenFDA.
|
| 8 |
+
|
| 9 |
+
**Hackathon Track:** `mcp-server-track`
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Usage
|
| 14 |
+
|
| 15 |
+
This application provides two tools, accessible via a tabbed interface or as MCP endpoints:
|
| 16 |
+
|
| 17 |
+
1. **Top Events**: For a given drug name, it returns a list and a bar chart of the top 10 most frequently reported adverse events.
|
| 18 |
+
2. **Event Frequency**: For a given drug and adverse event pair, it returns the total number of reports found.
|
| 19 |
+
|
| 20 |
+
## MCP Server Configuration
|
| 21 |
+
|
| 22 |
+
To use this application as a tool with an MCP client (like Cursor or Claude Desktop), you first need to run it and get the server URL.
|
| 23 |
+
|
| 24 |
+
1. **Run the App**:
|
| 25 |
+
```bash
|
| 26 |
+
python src/app.py
|
| 27 |
+
```
|
| 28 |
+
2. **Find the MCP URL**: The server will start and print the MCP URL in the console. It will look something like this:
|
| 29 |
+
`http://127.0.0.1:7860/gradio_api/mcp/sse`
|
| 30 |
+
3. **Configure Your Client**: Add the following configuration to your MCP client's settings, replacing the URL with the one from the previous step.
|
| 31 |
+
|
| 32 |
+
```json
|
| 33 |
+
{
|
| 34 |
+
"mcpServers": {
|
| 35 |
+
"ade_explorer": {
|
| 36 |
+
"url": "http://127.0.0.1:7860/gradio_api/mcp/sse"
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
The server exposes two tools: `top_adverse_events_tool(drug_name)` and `drug_event_stats_tool(drug_name, event_name)`.
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## Data Source and Disclaimer
|
| 47 |
+
|
| 48 |
+
* **Source**: All data is sourced from the public [FDA Adverse Event Reporting System (FAERS)](https://open.fda.gov/data/faers/) via the OpenFDA API.
|
| 49 |
+
* **Disclaimer**: The information provided by this tool is for informational purposes only and is based on spontaneous reports. It does not represent verified medical data or clinical proof of a causal relationship. **Always consult a qualified healthcare professional for medical advice.**
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio[mcp]>=5.0
|
| 2 |
+
pandas
|
| 3 |
+
requests
|
| 4 |
+
plotly
|
| 5 |
+
cachetools
|
| 6 |
+
pytest
|
src/app.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from src.openfda_client import get_top_adverse_events, get_drug_event_pair_frequency
|
| 3 |
+
from src.plotting import create_bar_chart
|
| 4 |
+
import pandas as pd
|
| 5 |
+
|
| 6 |
+
# --- Formatting Functions ---
|
| 7 |
+
|
| 8 |
+
def format_top_events_results(data: dict, drug_name: str) -> str:
|
| 9 |
+
"""Formats the results for the top adverse events tool."""
|
| 10 |
+
if "error" in data:
|
| 11 |
+
return f"An error occurred: {data['error']}"
|
| 12 |
+
|
| 13 |
+
if "results" not in data or not data["results"]:
|
| 14 |
+
return f"No adverse event data found for '{drug_name}'. The drug may not be in the database or it might be misspelled."
|
| 15 |
+
|
| 16 |
+
header = f"Top Adverse Events for '{drug_name.title()}'\n"
|
| 17 |
+
header += "Source: FDA FAERS via OpenFDA\n"
|
| 18 |
+
header += "Disclaimer: Spontaneous reports do not prove causation. Consult a healthcare professional.\n"
|
| 19 |
+
header += "---------------------------------------------------\n"
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
df = pd.DataFrame(data["results"])
|
| 23 |
+
df = df.rename(columns={"term": "Adverse Event", "count": "Report Count"})
|
| 24 |
+
result_string = df.to_string(index=False)
|
| 25 |
+
return header + result_string
|
| 26 |
+
except Exception as e:
|
| 27 |
+
return f"An error occurred while formatting the data: {e}"
|
| 28 |
+
|
| 29 |
+
def format_pair_frequency_results(data: dict, drug_name: str, event_name: str) -> str:
|
| 30 |
+
"""Formats the results for the drug-event pair frequency tool."""
|
| 31 |
+
if "error" in data:
|
| 32 |
+
return f"An error occurred: {data['error']}"
|
| 33 |
+
|
| 34 |
+
total_reports = data.get("meta", {}).get("results", {}).get("total", 0)
|
| 35 |
+
|
| 36 |
+
result = (
|
| 37 |
+
f"Found {total_reports:,} reports for the combination of "
|
| 38 |
+
f"'{drug_name.title()}' and '{event_name.title()}'.\n\n"
|
| 39 |
+
"Source: FDA FAERS via OpenFDA\n"
|
| 40 |
+
"Disclaimer: Spontaneous reports do not prove causation. Consult a healthcare professional."
|
| 41 |
+
)
|
| 42 |
+
return result
|
| 43 |
+
|
| 44 |
+
# --- Tool Functions ---
|
| 45 |
+
|
| 46 |
+
def top_adverse_events_tool(drug_name: str):
|
| 47 |
+
"""
|
| 48 |
+
MCP Tool: Finds the top reported adverse events for a given drug.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
drug_name (str): The name of the drug to search for.
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
tuple: A Plotly figure and a formatted string with the top adverse events.
|
| 55 |
+
"""
|
| 56 |
+
data = get_top_adverse_events(drug_name)
|
| 57 |
+
chart = create_bar_chart(data, drug_name)
|
| 58 |
+
text_summary = format_top_events_results(data, drug_name)
|
| 59 |
+
return chart, text_summary
|
| 60 |
+
|
| 61 |
+
def drug_event_stats_tool(drug_name: str, event_name: str):
|
| 62 |
+
"""
|
| 63 |
+
MCP Tool: Gets the total number of reports for a specific drug and adverse event pair.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
drug_name (str): The name of the drug to search for.
|
| 67 |
+
event_name (str): The name of the adverse event to search for.
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
str: A formatted string with the total count of reports.
|
| 71 |
+
"""
|
| 72 |
+
data = get_drug_event_pair_frequency(drug_name, event_name)
|
| 73 |
+
return format_pair_frequency_results(data, drug_name, event_name)
|
| 74 |
+
|
| 75 |
+
# --- Gradio Interface ---
|
| 76 |
+
|
| 77 |
+
interface1 = gr.Interface(
|
| 78 |
+
fn=top_adverse_events_tool,
|
| 79 |
+
inputs=[
|
| 80 |
+
gr.Textbox(
|
| 81 |
+
label="Drug Name",
|
| 82 |
+
info="Enter a brand or generic drug name (e.g., 'Aspirin', 'Lisinopril')."
|
| 83 |
+
)
|
| 84 |
+
],
|
| 85 |
+
outputs=[
|
| 86 |
+
gr.Plot(label="Top Adverse Events Chart"),
|
| 87 |
+
gr.Textbox(label="Top Adverse Events (Raw Data)", lines=15)
|
| 88 |
+
],
|
| 89 |
+
title="Top Adverse Events by Drug",
|
| 90 |
+
description="Find the most frequently reported adverse events for a specific medication.",
|
| 91 |
+
examples=[["Lisinopril"], ["Ozempic"], ["Metformin"]],
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
interface2 = gr.Interface(
|
| 95 |
+
fn=drug_event_stats_tool,
|
| 96 |
+
inputs=[
|
| 97 |
+
gr.Textbox(label="Drug Name", info="e.g., 'Ibuprofen'"),
|
| 98 |
+
gr.Textbox(label="Adverse Event", info="e.g., 'Headache'")
|
| 99 |
+
],
|
| 100 |
+
outputs=[gr.Textbox(label="Report Count", lines=5)],
|
| 101 |
+
title="Drug/Event Pair Frequency",
|
| 102 |
+
description="Get the total number of reports for a specific drug and adverse event combination.",
|
| 103 |
+
examples=[["Lisinopril", "Cough"], ["Ozempic", "Nausea"]],
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
demo = gr.TabbedInterface(
|
| 107 |
+
[interface1, interface2],
|
| 108 |
+
["Top Events", "Event Frequency"],
|
| 109 |
+
title="Medication Adverse-Event Explorer"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
if __name__ == "__main__":
|
| 113 |
+
demo.launch(mcp_server=True, server_name="0.0.0.0")
|
src/openfda_client.py
CHANGED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from cachetools import TTLCache, cached
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
API_BASE_URL = "https://api.fda.gov/drug/event.json"
|
| 6 |
+
# Cache with a TTL of 10 minutes (600 seconds)
|
| 7 |
+
cache = TTLCache(maxsize=256, ttl=600)
|
| 8 |
+
|
| 9 |
+
# 240 requests per minute per IP. A 0.25s delay is a simple way to stay under.
|
| 10 |
+
REQUEST_DELAY_SECONDS = 0.25
|
| 11 |
+
|
| 12 |
+
def get_top_adverse_events(drug_name: str, limit: int = 10) -> dict:
|
| 13 |
+
"""
|
| 14 |
+
Query OpenFDA to get the top adverse events for a given drug.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
drug_name (str): The name of the drug to search for (brand or generic).
|
| 18 |
+
limit (int): The maximum number of adverse events to return.
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
dict: The JSON response from the API, or an error dictionary.
|
| 22 |
+
"""
|
| 23 |
+
if not drug_name:
|
| 24 |
+
return {"error": "Drug name cannot be empty."}
|
| 25 |
+
|
| 26 |
+
drug_name_processed = drug_name.lower().strip()
|
| 27 |
+
|
| 28 |
+
# Using a simple cache key
|
| 29 |
+
cache_key = f"top_events_{drug_name_processed}_{limit}"
|
| 30 |
+
|
| 31 |
+
if cache_key in cache:
|
| 32 |
+
return cache[cache_key]
|
| 33 |
+
|
| 34 |
+
query = (
|
| 35 |
+
f'search=patient.drug.medicinalproduct:"{drug_name_processed}"'
|
| 36 |
+
f'&count=patient.reaction.reactionmeddrapt.exact&limit={limit}'
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
# Respect rate limits
|
| 41 |
+
time.sleep(REQUEST_DELAY_SECONDS)
|
| 42 |
+
|
| 43 |
+
response = requests.get(f"{API_BASE_URL}?{query}")
|
| 44 |
+
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
|
| 45 |
+
|
| 46 |
+
data = response.json()
|
| 47 |
+
cache[cache_key] = data
|
| 48 |
+
return data
|
| 49 |
+
|
| 50 |
+
except requests.exceptions.HTTPError as http_err:
|
| 51 |
+
if response.status_code == 404:
|
| 52 |
+
return {"error": f"No data found for drug: '{drug_name}'. It might be misspelled or not in the database."}
|
| 53 |
+
return {"error": f"HTTP error occurred: {http_err}"}
|
| 54 |
+
except requests.exceptions.RequestException as req_err:
|
| 55 |
+
return {"error": f"A network request error occurred: {req_err}"}
|
| 56 |
+
except Exception as e:
|
| 57 |
+
return {"error": f"An unexpected error occurred: {e}"}
|
| 58 |
+
|
| 59 |
+
def get_drug_event_pair_frequency(drug_name: str, event_name: str) -> dict:
|
| 60 |
+
"""
|
| 61 |
+
Query OpenFDA to get the total number of reports for a specific
|
| 62 |
+
drug-adverse event pair.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
drug_name (str): The name of the drug.
|
| 66 |
+
event_name (str): The name of the adverse event.
|
| 67 |
+
|
| 68 |
+
Returns:
|
| 69 |
+
dict: The JSON response from the API, or an error dictionary.
|
| 70 |
+
"""
|
| 71 |
+
if not drug_name or not event_name:
|
| 72 |
+
return {"error": "Drug name and event name cannot be empty."}
|
| 73 |
+
|
| 74 |
+
drug_name_processed = drug_name.lower().strip()
|
| 75 |
+
event_name_processed = event_name.lower().strip()
|
| 76 |
+
|
| 77 |
+
cache_key = f"pair_freq_{drug_name_processed}_{event_name_processed}"
|
| 78 |
+
if cache_key in cache:
|
| 79 |
+
return cache[cache_key]
|
| 80 |
+
|
| 81 |
+
query = (
|
| 82 |
+
f'search=patient.drug.medicinalproduct:"{drug_name_processed}"'
|
| 83 |
+
f'+AND+patient.reaction.reactionmeddrapt:"{event_name_processed}"'
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
try:
|
| 87 |
+
time.sleep(REQUEST_DELAY_SECONDS)
|
| 88 |
+
|
| 89 |
+
response = requests.get(f"{API_BASE_URL}?{query}")
|
| 90 |
+
response.raise_for_status()
|
| 91 |
+
|
| 92 |
+
data = response.json()
|
| 93 |
+
cache[cache_key] = data
|
| 94 |
+
return data
|
| 95 |
+
|
| 96 |
+
except requests.exceptions.HTTPError as http_err:
|
| 97 |
+
if response.status_code == 404:
|
| 98 |
+
return {"error": f"No data found for drug '{drug_name}' and event '{event_name}'. They may be misspelled or not in the database."}
|
| 99 |
+
return {"error": f"HTTP error occurred: {http_err}"}
|
| 100 |
+
except requests.exceptions.RequestException as req_err:
|
| 101 |
+
return {"error": f"A network request error occurred: {req_err}"}
|
| 102 |
+
except Exception as e:
|
| 103 |
+
return {"error": f"An unexpected error occurred: {e}"}
|
src/plotting.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import plotly.graph_objects as go
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
def create_bar_chart(data: dict, drug_name: str):
|
| 5 |
+
"""
|
| 6 |
+
Creates a Plotly bar chart from the OpenFDA data.
|
| 7 |
+
|
| 8 |
+
Args:
|
| 9 |
+
data (dict): The data from the OpenFDA client.
|
| 10 |
+
drug_name (str): The name of the drug.
|
| 11 |
+
|
| 12 |
+
Returns:
|
| 13 |
+
A Plotly Figure object if data is valid, otherwise None.
|
| 14 |
+
"""
|
| 15 |
+
if "error" in data or "results" not in data or not data["results"]:
|
| 16 |
+
return None
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
df = pd.DataFrame(data["results"])
|
| 20 |
+
df = df.rename(columns={"term": "Adverse Event", "count": "Report Count"})
|
| 21 |
+
|
| 22 |
+
# Ensure 'Report Count' is numeric
|
| 23 |
+
df['Report Count'] = pd.to_numeric(df['Report Count'])
|
| 24 |
+
|
| 25 |
+
# Sort for better visualization
|
| 26 |
+
df = df.sort_values(by="Report Count", ascending=True)
|
| 27 |
+
|
| 28 |
+
fig = go.Figure(
|
| 29 |
+
go.Bar(
|
| 30 |
+
x=df["Report Count"],
|
| 31 |
+
y=df["Adverse Event"],
|
| 32 |
+
orientation='h',
|
| 33 |
+
marker=dict(color='skyblue')
|
| 34 |
+
)
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
fig.update_layout(
|
| 38 |
+
title_text=f"Top Reported Adverse Events for {drug_name.title()}",
|
| 39 |
+
xaxis_title="Number of Reports",
|
| 40 |
+
yaxis_title="Adverse Event",
|
| 41 |
+
yaxis=dict(automargin=True),
|
| 42 |
+
height=max(400, len(df) * 30) # Dynamically adjust height
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
return fig
|
| 46 |
+
except Exception:
|
| 47 |
+
return None
|
tests/test_openfda_client.py
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from unittest.mock import patch, MagicMock
|
| 3 |
+
import requests
|
| 4 |
+
from src.openfda_client import get_top_adverse_events, get_drug_event_pair_frequency, cache
|
| 5 |
+
|
| 6 |
+
@pytest.fixture(autouse=True)
|
| 7 |
+
def clear_cache():
|
| 8 |
+
"""Fixture to clear the cache before each test."""
|
| 9 |
+
cache.clear()
|
| 10 |
+
|
| 11 |
+
def mock_response(status_code=200, json_data=None, raise_for_status=None):
|
| 12 |
+
"""Helper function to create a mock response object."""
|
| 13 |
+
mock_resp = MagicMock()
|
| 14 |
+
mock_resp.status_code = status_code
|
| 15 |
+
mock_resp.json.return_value = json_data
|
| 16 |
+
if raise_for_status:
|
| 17 |
+
mock_resp.raise_for_status.side_effect = raise_for_status
|
| 18 |
+
return mock_resp
|
| 19 |
+
|
| 20 |
+
@patch('requests.get')
|
| 21 |
+
def test_get_top_adverse_events_success(mock_get):
|
| 22 |
+
"""Test successful API call for top adverse events."""
|
| 23 |
+
mock_json = {"results": [{"term": "Nausea", "count": 100}]}
|
| 24 |
+
mock_get.return_value = mock_response(json_data=mock_json)
|
| 25 |
+
|
| 26 |
+
result = get_top_adverse_events("testdrug")
|
| 27 |
+
|
| 28 |
+
assert result == mock_json
|
| 29 |
+
mock_get.assert_called_once()
|
| 30 |
+
|
| 31 |
+
@patch('requests.get')
|
| 32 |
+
def test_get_top_adverse_events_404(mock_get):
|
| 33 |
+
"""Test 404 Not Found error for top adverse events."""
|
| 34 |
+
http_error = requests.exceptions.HTTPError("404 Client Error")
|
| 35 |
+
mock_get.return_value = mock_response(status_code=404, raise_for_status=http_error)
|
| 36 |
+
|
| 37 |
+
result = get_top_adverse_events("nonexistentdrug")
|
| 38 |
+
|
| 39 |
+
assert "error" in result
|
| 40 |
+
assert "No data found" in result["error"]
|
| 41 |
+
|
| 42 |
+
@patch('requests.get')
|
| 43 |
+
def test_get_drug_event_pair_frequency_success(mock_get):
|
| 44 |
+
"""Test successful API call for drug-event pair frequency."""
|
| 45 |
+
mock_json = {"meta": {"results": {"total": 50}}}
|
| 46 |
+
mock_get.return_value = mock_response(json_data=mock_json)
|
| 47 |
+
|
| 48 |
+
result = get_drug_event_pair_frequency("testdrug", "testevent")
|
| 49 |
+
|
| 50 |
+
assert result == mock_json
|
| 51 |
+
mock_get.assert_called_once()
|
| 52 |
+
|
| 53 |
+
def test_empty_drug_name_returns_error():
|
| 54 |
+
"""Test that empty inputs are handled correctly without calling the API."""
|
| 55 |
+
result = get_top_adverse_events("")
|
| 56 |
+
assert "error" in result
|
| 57 |
+
|
| 58 |
+
result2 = get_drug_event_pair_frequency("", "testevent")
|
| 59 |
+
assert "error" in result2
|
| 60 |
+
|
| 61 |
+
@patch('requests.get')
|
| 62 |
+
def test_caching_works(mock_get):
|
| 63 |
+
"""Test that results are cached to avoid repeated API calls."""
|
| 64 |
+
mock_json = {"results": [{"term": "Headache", "count": 200}]}
|
| 65 |
+
mock_get.return_value = mock_response(json_data=mock_json)
|
| 66 |
+
|
| 67 |
+
# First call - should call the API
|
| 68 |
+
get_top_adverse_events("cacheddrug")
|
| 69 |
+
assert mock_get.call_count == 1
|
| 70 |
+
|
| 71 |
+
# Second call - should hit the cache
|
| 72 |
+
get_top_adverse_events("cacheddrug")
|
| 73 |
+
assert mock_get.call_count == 1 # Still 1, not 2
|
| 74 |
+
|
| 75 |
+
# Call with different params - should trigger a new API call
|
| 76 |
+
get_top_adverse_events("cacheddrug", limit=20)
|
| 77 |
+
assert mock_get.call_count == 2
|