Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
F
FinSim
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Fares
FinSim
Commits
7dfba0af
Commit
7dfba0af
authored
May 02, 2026
by
Fares
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Migrate backend to Supabase PostgreSQL
parent
57b01db5
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
98 additions
and
80 deletions
+98
-80
app.py
investment_engine/app.py
+29
-18
config.py
investment_engine/config.py
+2
-6
railway.toml
investment_engine/railway.toml
+9
-0
requirements.txt
investment_engine/requirements.txt
+23
-0
database.py
investment_engine/services/database.py
+35
-56
No files found.
investment_engine/app.py
View file @
7dfba0af
...
...
@@ -8,9 +8,11 @@ import uuid
import
bcrypt
from
datetime
import
datetime
from
fastapi
import
FastAPI
,
HTTPException
,
Request
,
Depends
,
UploadFile
,
File
,
Form
from
fastapi.middleware.cors
import
CORSMiddleware
from
fastapi.staticfiles
import
StaticFiles
from
fastapi.responses
import
FileResponse
,
JSONResponse
from
pydantic
import
BaseModel
,
Field
import
psycopg2.extras
from
typing
import
Optional
from
dotenv
import
load_dotenv
...
...
@@ -31,6 +33,15 @@ except Exception as e:
app
=
FastAPI
(
title
=
"FinSim"
,
version
=
"2.0.0"
)
# ── CORS — allow Flutter web (and any dev origin) ────────────────
app
.
add_middleware
(
CORSMiddleware
,
allow_origins
=
[
"*"
],
allow_credentials
=
True
,
allow_methods
=
[
"*"
],
allow_headers
=
[
"*"
],
)
# ── In-memory session store ──────────────────────────────────────
_tokens
:
dict
[
str
,
dict
]
=
{}
# token -> {user_id, username, role, ...}
...
...
@@ -89,7 +100,7 @@ def get_ui():
def
login
(
req
:
LoginRequest
):
from
services.database
import
get_connection
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
(
dictionary
=
True
)
cursor
=
conn
.
cursor
(
cursor_factory
=
psycopg2
.
extras
.
RealDictCursor
)
cursor
.
execute
(
"SELECT * FROM users WHERE email =
%
s"
,
(
req
.
email
,))
user
=
cursor
.
fetchone
()
...
...
@@ -131,10 +142,10 @@ def register(req: RegisterRequest):
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
cursor
.
execute
(
"INSERT INTO users (username, email, password, role) VALUES (
%
s,
%
s,
%
s, 'user')"
,
"INSERT INTO users (username, email, password, role) VALUES (
%
s,
%
s,
%
s, 'user')
RETURNING id
"
,
(
req
.
username
,
req
.
email
,
hashed
)
)
user_id
=
cursor
.
lastrowid
user_id
=
cursor
.
fetchone
()[
0
]
except
Exception
as
e
:
if
"Duplicate"
in
str
(
e
):
raise
HTTPException
(
status_code
=
409
,
detail
=
"Username or email already exists"
)
...
...
@@ -158,7 +169,7 @@ def get_me(user: dict = Depends(get_current_user)):
# Refresh from DB
from
services.database
import
get_connection
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
(
dictionary
=
True
)
cursor
=
conn
.
cursor
(
cursor_factory
=
psycopg2
.
extras
.
RealDictCursor
)
cursor
.
execute
(
"SELECT total_score, quizzes_taken FROM users WHERE id =
%
s"
,
(
user
[
"user_id"
],))
fresh
=
cursor
.
fetchone
()
if
fresh
:
...
...
@@ -369,7 +380,7 @@ def get_scenarios(page: int = 1, limit: int = 12, difficulty: str = "", category
where_sql
=
" AND "
.
join
(
where_clauses
)
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
(
dictionary
=
True
)
cursor
=
conn
.
cursor
(
cursor_factory
=
psycopg2
.
extras
.
RealDictCursor
)
cursor
.
execute
(
f
"SELECT COUNT(*) as total FROM scenarios WHERE {where_sql}"
,
params
)
total
=
cursor
.
fetchone
()[
"total"
]
...
...
@@ -393,7 +404,7 @@ def get_scenarios(page: int = 1, limit: int = 12, difficulty: str = "", category
def
get_scenario_detail
(
scenario_id
:
str
):
from
services.database
import
get_connection
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
(
dictionary
=
True
)
cursor
=
conn
.
cursor
(
cursor_factory
=
psycopg2
.
extras
.
RealDictCursor
)
cursor
.
execute
(
"SELECT * FROM scenarios WHERE id =
%
s"
,
(
scenario_id
,))
scenario
=
cursor
.
fetchone
()
if
not
scenario
:
...
...
@@ -430,7 +441,7 @@ def start_quiz(req: StartQuizRequest, user: dict = Depends(get_current_user)):
where_sql
=
" AND "
.
join
(
where_clauses
)
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
(
dictionary
=
True
)
cursor
=
conn
.
cursor
(
cursor_factory
=
psycopg2
.
extras
.
RealDictCursor
)
# Get random scenarios
cursor
.
execute
(
...
...
@@ -448,10 +459,10 @@ def start_quiz(req: StartQuizRequest, user: dict = Depends(get_current_user)):
# Create quiz
cursor
.
execute
(
"INSERT INTO quizzes (title, description, time_limit, passing_score, created_by) VALUES (
%
s,
%
s,
%
s,
%
s,
%
s)"
,
"INSERT INTO quizzes (title, description, time_limit, passing_score, created_by) VALUES (
%
s,
%
s,
%
s,
%
s,
%
s)
RETURNING id
"
,
(
f
"Quick Quiz - {datetime.now().strftime('
%
b
%
d,
%
H:
%
M')}"
,
f
"{len(scenarios)} questions"
,
0
,
60
,
user
[
"user_id"
])
)
quiz_id
=
cursor
.
lastrowid
quiz_id
=
cursor
.
fetchone
()[
'id'
]
# Link scenarios
for
i
,
s
in
enumerate
(
scenarios
):
...
...
@@ -462,10 +473,10 @@ def start_quiz(req: StartQuizRequest, user: dict = Depends(get_current_user)):
# Create attempt
cursor
.
execute
(
"INSERT INTO quiz_attempts (user_id, quiz_id, total_questions, status) VALUES (
%
s,
%
s,
%
s, 'in_progress')"
,
"INSERT INTO quiz_attempts (user_id, quiz_id, total_questions, status) VALUES (
%
s,
%
s,
%
s, 'in_progress')
RETURNING id
"
,
(
user
[
"user_id"
],
quiz_id
,
len
(
scenarios
))
)
attempt_id
=
cursor
.
lastrowid
attempt_id
=
cursor
.
fetchone
()[
'id'
]
# Process scenarios for frontend
questions
=
[]
...
...
@@ -526,7 +537,7 @@ def submit_quiz(attempt_id: int, answers: list[AnswerRequest], user: dict = Depe
from
services.database
import
get_connection
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
(
dictionary
=
True
)
cursor
=
conn
.
cursor
(
cursor_factory
=
psycopg2
.
extras
.
RealDictCursor
)
# Verify attempt belongs to user
cursor
.
execute
(
"SELECT * FROM quiz_attempts WHERE id =
%
s AND user_id =
%
s"
,
(
attempt_id
,
user
[
"user_id"
]))
...
...
@@ -551,7 +562,7 @@ def submit_quiz(attempt_id: int, answers: list[AnswerRequest], user: dict = Depe
# Record history
cursor
.
execute
(
"INSERT INTO user_scenario_history (user_id, scenario_id, selected_answer, is_correct, attempt_id) VALUES (
%
s,
%
s,
%
s,
%
s,
%
s)"
,
(
user
[
"user_id"
],
ans
.
scenario_id
,
ans
.
selected_answer
,
1
if
is_correct
else
0
,
attempt_id
)
(
user
[
"user_id"
],
ans
.
scenario_id
,
ans
.
selected_answer
,
is_correct
,
attempt_id
)
)
# Build explanation
...
...
@@ -604,7 +615,7 @@ def submit_quiz(attempt_id: int, answers: list[AnswerRequest], user: dict = Depe
def
get_user_stats
(
user
:
dict
=
Depends
(
get_current_user
)):
from
services.database
import
get_connection
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
(
dictionary
=
True
)
cursor
=
conn
.
cursor
(
cursor_factory
=
psycopg2
.
extras
.
RealDictCursor
)
# Get overall stats
cursor
.
execute
(
"SELECT total_score, quizzes_taken FROM users WHERE id =
%
s"
,
(
user
[
"user_id"
],))
...
...
@@ -627,14 +638,14 @@ def get_user_stats(user: dict = Depends(get_current_user)):
# Get accuracy
cursor
.
execute
(
"SELECT COUNT(*) as total, SUM(is_correct) as correct FROM user_scenario_history WHERE user_id =
%
s"
,
"SELECT COUNT(*) as total, SUM(is_correct
::int
) as correct FROM user_scenario_history WHERE user_id =
%
s"
,
(
user
[
"user_id"
],)
)
accuracy
=
cursor
.
fetchone
()
# Get category breakdown
cursor
.
execute
(
"""SELECT s.category, COUNT(*) as attempts, SUM(ush.is_correct) as correct
"""SELECT s.category, COUNT(*) as attempts, SUM(ush.is_correct
::int
) as correct
FROM user_scenario_history ush
JOIN scenarios s ON ush.scenario_id = s.id
WHERE ush.user_id =
%
s
...
...
@@ -659,7 +670,7 @@ def get_user_stats(user: dict = Depends(get_current_user)):
def
get_leaderboard
():
from
services.database
import
get_connection
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
(
dictionary
=
True
)
cursor
=
conn
.
cursor
(
cursor_factory
=
psycopg2
.
extras
.
RealDictCursor
)
cursor
.
execute
(
"""SELECT username, total_score, quizzes_taken, avatar,
CASE WHEN quizzes_taken > 0 THEN ROUND(total_score / quizzes_taken) ELSE 0 END as avg_score
...
...
@@ -679,7 +690,7 @@ def get_leaderboard():
def
get_filters
():
from
services.database
import
get_connection
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
(
dictionary
=
True
)
cursor
=
conn
.
cursor
(
cursor_factory
=
psycopg2
.
extras
.
RealDictCursor
)
cursor
.
execute
(
"SELECT DISTINCT category FROM scenarios WHERE is_active = 1 AND category IS NOT NULL"
)
categories
=
[
r
[
"category"
]
for
r
in
cursor
.
fetchall
()]
cursor
.
execute
(
"SELECT DISTINCT difficulty FROM scenarios WHERE is_active = 1 AND difficulty IS NOT NULL"
)
...
...
investment_engine/config.py
View file @
7dfba0af
...
...
@@ -16,12 +16,8 @@ class Settings(BaseSettings):
# ── SerpAPI ─────────────────────────────────────────────
SERPAPI_API_KEY
:
str
=
""
# ── MySQL (Al-Arcade Remote DB) ───────────────────────
MYSQL_HOST
:
str
=
"scenariodb.caprover.al-arcade.com"
MYSQL_PORT
:
int
=
3306
MYSQL_USER
:
str
=
"root"
MYSQL_PASSWORD
:
str
=
"Alarcade123#"
MYSQL_DATABASE
:
str
=
"mcq_app"
# ── Supabase (PostgreSQL) ─────────────────────────────────
SUPABASE_DB_URL
:
str
=
""
# ── Defaults for Z-Score ──────────────────────────────
DEFAULT_ZSCORE_WINDOW
:
int
=
100
...
...
investment_engine/railway.toml
0 → 100644
View file @
7dfba0af
[build]
builder
=
"DOCKERFILE"
dockerfilePath
=
"Dockerfile"
[deploy]
healthcheckPath
=
"/"
healthcheckTimeout
=
300
restartPolicyType
=
"ON_FAILURE"
restartPolicyMaxRetries
=
3
investment_engine/requirements.txt
0 → 100644
View file @
7dfba0af
bcrypt>=5.0.0
beautifulsoup4>=4.14.3
chromadb>=1.5.5
fastapi>=0.135.1
langchain>=0.3.0
langchain-chroma>=1.1.0
langchain-community>=0.4.1
langchain-core>=0.3.0
langchain-groq>=0.2.0
langchain-huggingface>=1.2.1
langgraph>=1.1.1
psycopg2-binary>=2.9.9
numpy>=1.24
polars>=1.38.1
pyarrow>=23.0.1
pydantic>=2.12.5
pydantic-settings>=2.13.1
pypdf>=6.9.2
python-dotenv>=1.2.2
python-multipart>=0.0.22
sentence-transformers>=5.3.0
uvicorn>=0.41.0
yfinance>=1.2.0
investment_engine/services/database.py
View file @
7dfba0af
"""
Database layer —
MySQL operations connecting directly to CapRover instanc
e.
Database layer —
PostgreSQL operations connecting directly to Supabas
e.
"""
import
json
import
mysql.connector
from
mysql.connector
import
pooling
import
os
import
psycopg2
from
psycopg2
import
pool
from
contextlib
import
contextmanager
from
config
import
get_settings
from
models
import
Scenario
,
ScenarioGenerationResult
_pool
:
pool
.
SimpleConnectionPool
|
None
=
None
_pool
:
pooling
.
MySQLConnectionPool
|
None
=
None
def
_get_pool
()
->
pooling
.
MySQLConnectionPool
:
def
_get_pool
()
->
pool
.
SimpleConnectionPool
:
global
_pool
if
_pool
is
None
:
s
=
get_settings
()
_pool
=
pooling
.
MySQLConnectionPool
(
pool_name
=
"scenario_pool"
,
pool_size
=
5
,
host
=
s
.
MYSQL_HOST
,
port
=
s
.
MYSQL_PORT
,
user
=
s
.
MYSQL_USER
,
password
=
s
.
MYSQL_PASSWORD
,
d
atabase
=
s
.
MYSQL_DATABASE
,
# Fallback to SUPABASE_DB_URL from env or settings
db_url
=
os
.
environ
.
get
(
"SUPABASE_DB_URL"
)
or
s
.
SUPABASE_DB_URL
if
not
db_url
:
raise
ValueError
(
"SUPABASE_DB_URL is not set."
)
_pool
=
pool
.
SimpleConnectionPool
(
1
,
10
,
d
sn
=
db_url
)
return
_pool
@
contextmanager
def
get_connection
():
conn
=
_get_pool
()
.
get_connection
()
conn_pool
=
_get_pool
()
conn
=
conn_pool
.
getconn
()
try
:
yield
conn
conn
.
commit
()
...
...
@@ -40,36 +40,12 @@ def get_connection():
conn
.
rollback
()
raise
finally
:
conn
.
close
(
)
conn
_pool
.
putconn
(
conn
)
def
init_db
():
"""Create the scenarios table if it doesn't exist."""
ddl
=
"""
CREATE TABLE IF NOT EXISTS scenarios (
id VARCHAR(20) PRIMARY KEY,
title TEXT NOT NULL,
short_description TEXT,
givens_table JSON,
scenario_paragraph TEXT,
best_answer TEXT,
best_answer_rationale TEXT,
other_option1 TEXT,
other_option1_exp TEXT,
other_option2 TEXT,
other_option2_exp TEXT,
other_option3 TEXT,
other_option3_exp TEXT,
event_type VARCHAR(20) DEFAULT 'normal',
difficulty VARCHAR(20) DEFAULT 'medium',
category VARCHAR(100) DEFAULT 'General',
risk_level VARCHAR(20) DEFAULT 'Medium',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
cursor
.
execute
(
ddl
)
"""No-op for Supabase as tables are managed via Supabase UI/Migrations."""
pass
def
insert_scenario
(
s
:
Scenario
):
...
...
@@ -85,12 +61,12 @@ def insert_scenario(s: Scenario):
) VALUES (
%
s,
%
s,
%
s,
%
s,
%
s,
%
s,
%
s,
%
s,
%
s,
%
s,
%
s,
%
s,
%
s,
%
s,
%
s,
%
s,
%
s
)
ON
DUPLICATE KEY UPDATE
title =
VALUES(title)
,
short_description =
VALUES(short_description)
,
givens_table =
VALUES(givens_table)
,
scenario_paragraph =
VALUES(scenario_paragraph)
,
risk_level =
VALUES(risk_level)
ON
CONFLICT (id) DO UPDATE SET
title =
EXCLUDED.title
,
short_description =
EXCLUDED.short_description
,
givens_table =
EXCLUDED.givens_table
,
scenario_paragraph =
EXCLUDED.scenario_paragraph
,
risk_level =
EXCLUDED.risk_level
"""
# Pad extra options if Gemini returns less than 3
...
...
@@ -124,12 +100,12 @@ def insert_scenario(s: Scenario):
)
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
cursor
.
execute
(
sql
,
params
)
with
conn
.
cursor
()
as
cursor
:
cursor
.
execute
(
sql
,
params
)
def
insert_all_scenarios
(
result
:
ScenarioGenerationResult
)
->
int
:
"""Bulk insert all scenarios to
CapRover
database. Returns count inserted."""
"""Bulk insert all scenarios to
Supabase
database. Returns count inserted."""
count
=
0
for
s
in
result
.
scenarios
:
insert_scenario
(
s
)
...
...
@@ -147,11 +123,14 @@ def get_random_scenario() -> dict | None:
other_option3, other_option3_exp,
event_type, difficulty, category, risk_level
FROM scenarios
ORDER BY RAND()
ORDER BY RAND
OM
()
LIMIT 1
"""
import
psycopg2.extras
with
get_connection
()
as
conn
:
cursor
=
conn
.
cursor
(
dictionary
=
True
)
cursor
.
execute
(
sql
)
row
=
cursor
.
fetchone
()
with
conn
.
cursor
(
cursor_factory
=
psycopg2
.
extras
.
RealDictCursor
)
as
cursor
:
cursor
.
execute
(
sql
)
row
=
cursor
.
fetchone
()
if
row
:
row
=
dict
(
row
)
return
row
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment