Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions docker-compose.web.yml → docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
# VM / server deploy for OPM web app (testcase.md). Do not commit secrets.
# Usage:
# export OPM_MODEL=your-model
# export OPENAI_API_KEY=your-key
# docker compose -f docker-compose.web.yml up -d --build
#
# SQLite persists in Docker volume `opm_web_data`. UI: http://HOST:8000/

services:
opm-web:
web:
build:
context: .
dockerfile: Dockerfile.web
ports:
- "8000:8000"
- "3000:8000"
environment:
WEB_DATA_DIR: /data
DEPLOY_TYPE: ${DEPLOY_TYPE:-DEV}
OPM_MODEL: ${OPM_MODEL:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-}
OPM_OPENAI_EXTRA_HEADERS: ${OPM_OPENAI_EXTRA_HEADERS:-}
OPM_TOP_P: ${OPM_TOP_P:-}
volumes:
- opm_web_data:/data
- web_data:/data

volumes:
opm_web_data:
web_data:
238 changes: 113 additions & 125 deletions web/app/db.py
Original file line number Diff line number Diff line change
@@ -1,180 +1,168 @@
from __future__ import annotations

import json as _json
import os
import sqlite3
from pathlib import Path
from typing import Optional

from dotenv import load_dotenv
from sqlalchemy import create_engine, text
from sqlalchemy.engine import URL

load_dotenv() # loads .env; system env vars override .env values

BASE_DIR = Path(__file__).resolve().parents[1]
_data_override = os.environ.get("WEB_DATA_DIR", "").strip()
DATA_DIR = Path(_data_override) if _data_override else BASE_DIR / "data"
DB_PATH = DATA_DIR / "app.db"


def ensure_data_directory_exists() -> None:
INITDB_SQLITE = Path(__file__).parent / "initdb_sqlite.sql"
INITDB_MYSQL = Path(__file__).parent / "initdb_mysql.sql"

DEPLOY_TYPE = os.environ.get("DEPLOY_TYPE", "DEV").strip().upper()

if DEPLOY_TYPE == "PROD":
url = URL.create(
drivername="mysql+pymysql",
username=os.environ.get("MYSQL_USER", "internta_user"),
password=os.environ["MYSQL_PASSWORD"],
host=os.environ.get("MYSQL_HOST", "mysql6.sqlpub.com"),
port=int(os.environ.get("MYSQL_PORT", "3311")),
database=os.environ.get("MYSQL_DATABASE", "internta_db"),
)
else:
DATA_DIR.mkdir(parents=True, exist_ok=True)
url = URL.create(
drivername="sqlite+pysqlite",
database=str(DB_PATH),
)


def get_connection() -> sqlite3.Connection:
ensure_data_directory_exists()
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
return connection
engine = create_engine(url)


def init_db() -> None:
ensure_data_directory_exists()
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
"""
)
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS action_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER,
text TEXT NOT NULL,
done INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (note_id) REFERENCES notes(id)
);
"""
)
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS opm_diagrams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER,
payload TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
"""
)
connection.commit()
sql_file = INITDB_MYSQL if DEPLOY_TYPE == "PROD" else INITDB_SQLITE
sql = Path(sql_file).read_text()
statements = [s.strip() for s in sql.split(";") if s.strip()]
with engine.begin() as conn:
for stmt in statements:
conn.execute(text(stmt))


def _last_insert_id(result) -> int:
return int(result.lastrowid)


def insert_note(content: str) -> int:
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute("INSERT INTO notes (content) VALUES (?)", (content,))
connection.commit()
return int(cursor.lastrowid)


def list_notes() -> list[sqlite3.Row]:
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute("SELECT id, content, created_at FROM notes ORDER BY id DESC")
return list(cursor.fetchall())


def get_note(note_id: int) -> Optional[sqlite3.Row]:
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute(
"SELECT id, content, created_at FROM notes WHERE id = ?",
(note_id,),
with engine.begin() as conn:
result = conn.execute(
text("INSERT INTO notes (content) VALUES (:content)"),
{"content": content},
)
row = cursor.fetchone()
return row
return _last_insert_id(result)


def list_notes() -> list:
with engine.connect() as conn:
return list(
conn.execute(
text("SELECT id, content, created_at FROM notes ORDER BY id DESC")
).mappings().fetchall()
)


def get_note(note_id: int) -> Optional[object]:
with engine.connect() as conn:
return conn.execute(
text("SELECT id, content, created_at FROM notes WHERE id = :id"),
{"id": note_id},
).mappings().fetchone()


def insert_action_items(items: list[str], note_id: Optional[int] = None) -> list[int]:
with get_connection() as connection:
cursor = connection.cursor()
ids: list[int] = []
ids: list[int] = []
with engine.begin() as conn:
for item in items:
cursor.execute(
"INSERT INTO action_items (note_id, text) VALUES (?, ?)",
(note_id, item),
result = conn.execute(
text("INSERT INTO action_items (note_id, text) VALUES (:note_id, :text)"),
{"note_id": note_id, "text": item},
)
ids.append(int(cursor.lastrowid))
connection.commit()
return ids
ids.append(_last_insert_id(result))
return ids


def list_action_items(note_id: Optional[int] = None) -> list[sqlite3.Row]:
with get_connection() as connection:
cursor = connection.cursor()
def list_action_items(note_id: Optional[int] = None) -> list:
with engine.connect() as conn:
if note_id is None:
cursor.execute(
"SELECT id, note_id, text, done, created_at FROM action_items ORDER BY id DESC"
)
rows = conn.execute(
text(
"SELECT id, note_id, text, done, created_at"
" FROM action_items ORDER BY id DESC"
)
).mappings().fetchall()
else:
cursor.execute(
"SELECT id, note_id, text, done, created_at FROM action_items WHERE note_id = ? ORDER BY id DESC",
(note_id,),
)
return list(cursor.fetchall())
rows = conn.execute(
text(
"SELECT id, note_id, text, done, created_at"
" FROM action_items WHERE note_id = :note_id ORDER BY id DESC"
),
{"note_id": note_id},
).mappings().fetchall()
return list(rows)


def mark_action_item_done(action_item_id: int, done: bool) -> None:
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute(
"UPDATE action_items SET done = ? WHERE id = ?",
(1 if done else 0, action_item_id),
with engine.begin() as conn:
conn.execute(
text("UPDATE action_items SET done = :done WHERE id = :id"),
{"done": 1 if done else 0, "id": action_item_id},
)
connection.commit()


import json as _json


def insert_opm_diagram(payload: dict, note_id: Optional[int] = None) -> int:
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute(
"INSERT INTO opm_diagrams (note_id, payload) VALUES (?, ?)",
(note_id, _json.dumps(payload)),
with engine.begin() as conn:
result = conn.execute(
text(
"INSERT INTO opm_diagrams (note_id, payload) VALUES (:note_id, :payload)"
),
{"note_id": note_id, "payload": _json.dumps(payload)},
)
connection.commit()
return int(cursor.lastrowid)
return _last_insert_id(result)


def list_opm_diagrams(limit: Optional[int] = None) -> list[dict]:
q = "SELECT id, note_id, payload, created_at FROM opm_diagrams ORDER BY id DESC"
params: tuple = ()
params: dict = {}
if limit is not None:
q += " LIMIT ?"
params = (int(limit),)
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute(q, params)
rows = cursor.fetchall()
q += " LIMIT :limit"
params["limit"] = int(limit)
with engine.connect() as conn:
rows = conn.execute(text(q), params).fetchall()
return [
{
"id": r["id"],
"note_id": r["note_id"],
"created_at": r["created_at"],
"diagram": _json.loads(r["payload"]),
"id": r.id,
"note_id": r.note_id,
"created_at": r.created_at,
"diagram": _json.loads(r.payload),
}
for r in rows
]


def get_opm_diagram(diagram_id: int) -> Optional[dict]:
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute(
"SELECT id, note_id, payload, created_at FROM opm_diagrams WHERE id = ?",
(diagram_id,),
)
row = cursor.fetchone()
with engine.connect() as conn:
row = conn.execute(
text(
"SELECT id, note_id, payload, created_at"
" FROM opm_diagrams WHERE id = :id"
),
{"id": diagram_id},
).fetchone()
if row is None:
return None
return {
"id": row["id"],
"note_id": row["note_id"],
"created_at": row["created_at"],
"diagram": _json.loads(row["payload"]),
"id": row.id,
"note_id": row.note_id,
"created_at": row.created_at,
"diagram": _json.loads(row.payload),
}

21 changes: 21 additions & 0 deletions web/app/initdb_mysql.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS notes (
id INT AUTO_INCREMENT PRIMARY KEY,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS action_items (
id INT AUTO_INCREMENT PRIMARY KEY,
note_id INT,
text TEXT NOT NULL,
done TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES notes(id)
);

CREATE TABLE IF NOT EXISTS opm_diagrams (
id INT AUTO_INCREMENT PRIMARY KEY,
note_id INT,
payload TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
21 changes: 21 additions & 0 deletions web/app/initdb_sqlite.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS action_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER,
text TEXT NOT NULL,
done INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES notes(id)
);

CREATE TABLE IF NOT EXISTS opm_diagrams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER,
payload TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Binary file modified web/data/app.db
Binary file not shown.
2 changes: 2 additions & 0 deletions web/requirements-min.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ uvicorn[standard]>=0.23.0
pydantic>=2.0.0
openai>=1.0.0
python-dotenv>=1.0.0
sqlalchemy>=2.0.0
pymysql>=1.1.0
Loading