implements cache

This commit is contained in:
2025-10-31 10:19:43 -07:00
parent 3de2a97d5c
commit 9e2e0bec36
4 changed files with 115 additions and 40 deletions

1
.gitignore vendored
View File

@@ -7,5 +7,6 @@ terraform/.terraform/**
**/terraform.tfstate **/terraform.tfstate
**/terraform.tfstate.backup **/terraform.tfstate.backup
.terraform/* .terraform/*
__pycache__/**

View File

@@ -17,4 +17,4 @@ COPY . .
EXPOSE 8080 EXPOSE 8080
# Command to run the application with gunicorn # Command to run the application with gunicorn
CMD exec gunicorn --bind :8080 --workers 1 --threads 8 --timeout 0 app:app CMD exec gunicorn --bind :8080 --workers 1 --threads 8 --timeout 0 app:app

110
app.py
View File

@@ -38,6 +38,9 @@ FV_USER_ID = os.environ.get("FILEVINE_USER_ID")
if not all([FV_CLIENT_ID, FV_CLIENT_SECRET, FV_PAT, FV_ORG_ID, FV_USER_ID]): if not all([FV_CLIENT_ID, FV_CLIENT_SECRET, FV_PAT, FV_ORG_ID, FV_USER_ID]):
print("[WARN] Missing one or more Filevine env vars — dashboard will fail until set.") print("[WARN] Missing one or more Filevine env vars — dashboard will fail until set.")
# --- Cache ---
from cache import project_cache
# --- Helpers --- # --- Helpers ---
def login_required(view): def login_required(view):
@@ -62,6 +65,55 @@ def get_user_profile(uid: str):
return {"enabled": bool(data.get("enabled", False)), "caseEmail": data.get("caseEmail")} return {"enabled": bool(data.get("enabled", False)), "caseEmail": data.get("caseEmail")}
def fetch_all_projects_for_user(uid: str):
"""Fetch all projects for a user and cache them"""
# Get bearer token
bearer = get_filevine_bearer()
# List projects (all pages)
projects = list_all_projects(bearer)
# Filter to ProjectEmailAddress == caseEmail
profile = get_user_profile(uid)
case_email = profile.get("caseEmail")
# if case_email:
# filtered = [p for p in projects if str(p.get("ProjectEmailAddress", "")).lower() == str(case_email).lower()]
# else:
# filtered = []
filtered = projects
# Fetch details for each
detailed_rows = []
for p in filtered:
pid = (p.get("projectId") or {}).get("native")
c = fetch_client(bearer, (p.get("clientId") or {}).get("native"))
cs = fetch_contacts(bearer, pid)
print("CS IS", cs)
if pid is None:
continue
try:
detail = fetch_project_detail(bearer, pid)
except Exception as e:
print(f"[WARN] detail fetch failed for {pid}: {e}")
detail = {}
row = {
"client": c.get("firstName"),
"matter_description": p.get("projectName"),
"contacts": cs,
"ProjectEmailAddress": p.get("projectEmailAddress"),
"Number": p.get("number"),
"IncidentDate": (p.get("incidentDate") or detail.get("incidentDate")),
"ProjectId": pid,
"ProjectName": p.get("projectName") or detail.get("projectName"),
"ProjectUrl": p.get("projectUrl") or detail.get("projectUrl"),
}
detailed_rows.append(row)
# Cache the results
project_cache.set_projects(detailed_rows)
return detailed_rows
# --- Routes --- # --- Routes ---
@app.route("/") @app.route("/")
def index(): def index():
@@ -99,6 +151,13 @@ def session_login():
session["uid"] = uid session["uid"] = uid
# Optional: short session # Optional: short session
session["expires_at"] = (datetime.utcnow() + timedelta(hours=8)).isoformat() session["expires_at"] = (datetime.utcnow() + timedelta(hours=8)).isoformat()
# Async update cache after login
from threading import Thread
thread = Thread(target=fetch_all_projects_for_user, args=(uid,))
thread.daemon = True
thread.start()
return jsonify({"ok": True}) return jsonify({"ok": True})
except Exception as e: except Exception as e:
print("[ERR] session_login:", e) print("[ERR] session_login:", e)
@@ -224,48 +283,21 @@ def dashboard():
if not case_email: if not case_email:
return redirect(url_for("welcome")) return redirect(url_for("welcome"))
# 1) Bearer token # Check cache first
bearer = get_filevine_bearer() cached_projects = project_cache.get_projects()
if cached_projects is not None:
# 2) List projects (all pages) detailed_rows = cached_projects
projects = list_all_projects(bearer) print("USING CACHE")
else:
# 3) Filter to ProjectEmailAddress == caseEmail # Fetch and cache projects
#filtered = [p for p in projects if str(p.get("ProjectEmailAddress", "")).lower() == str(case_email).lower()] detailed_rows = fetch_all_projects_for_user(uid)
print("FETCHING")
filtered = projects
# 4) Fetch details for each
detailed_rows = []
for p in filtered:
pid = (p.get("projectId") or {}).get("native")
c = fetch_client(bearer, (p.get("clientId") or {}).get("native"))
cs = fetch_contacts(bearer, pid)
print("CS IS", cs)
if pid is None:
continue
try:
detail = fetch_project_detail(bearer, pid)
except Exception as e:
print(f"[WARN] detail fetch failed for {pid}: {e}")
detail = {}
row = {
"client": c.get("firstName"),
"matter_description": p.get("projectName"),
"contacts": cs,
"ProjectEmailAddress": p.get("projectEmailAddress"),
"Number": p.get("number"),
"IncidentDate": (p.get("incidentDate") or detail.get("incidentDate")),
"ProjectId": pid,
"ProjectName": p.get("projectName") or detail.get("projectName"),
"ProjectUrl": p.get("projectUrl") or detail.get("projectUrl"),
}
detailed_rows.append(row)
print("HI", len(detailed_rows))
# 5) Render table # 5) Render table
return render_template("dashboard.html", rows=detailed_rows, case_email=case_email) return render_template("dashboard.html", rows=detailed_rows, case_email=case_email)
# GAE compatibility # GAE compatibility
if __name__ == "__main__" if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=8080) app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", "5004")))

42
cache.py Normal file
View File

@@ -0,0 +1,42 @@
# In-memory cache for Filevine projects
import threading
import time
from datetime import datetime, timedelta
class ProjectCache:
def __init__(self):
self._cache = {}
self._lock = threading.Lock()
self._last_updated = None
self._is_updating = False
def get_projects(self):
"""Get cached projects if they exist and are not expired (15 minutes)"""
with self._lock:
if not self._cache or not self._last_updated:
return None
# Check if cache is older than 15 minutes
if datetime.now() - self._last_updated > timedelta(minutes=15):
return None
return self._cache.copy()
def set_projects(self, projects):
"""Set projects in cache with current timestamp"""
with self._lock:
self._cache = projects.copy() if projects else {}
self._last_updated = datetime.now()
def is_updating(self):
"""Check if cache is currently being updated"""
with self._lock:
return self._is_updating
def set_updating(self, updating):
"""Set the updating status"""
with self._lock:
self._is_updating = updating
# Global cache instance
project_cache = ProjectCache()