Automatic flex query syncing

This commit is contained in:
2026-04-01 23:28:51 -05:00
parent aa872aba67
commit cfe00f67b5
6 changed files with 145 additions and 16 deletions

View File

@@ -16,6 +16,19 @@ from flex_query import FlexQueryError, fetch_flex_statement, get_flex_status
from ib_client import IBClient
import time
FLEX_AUTO_SYNC_MAX_AGE_SECONDS = 8 * 60 * 60
_flex_sync_lock = threading.Lock()
_flex_startup_sync_started = False
_flex_sync_state = {
"last_attempt_at": None,
"last_success_at": None,
"last_result": None,
"last_error": None,
"last_saved_to": None,
"last_reference_code": None,
"last_trigger": None,
}
def load_dotenv_file() -> None:
env_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env"))
@@ -41,18 +54,92 @@ load_dotenv_file()
# Use an absolute path for the static folder so Flask can reliably serve files
STATIC_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "frontend"))
app = Flask(__name__, static_folder=STATIC_FOLDER, static_url_path="/static")
# instantiate the IB client with a process-unique client id to avoid
# collisions when Flask's reloader spawns multiple processes.
ib = IBClient(client_id=os.getpid())
app = Flask(__name__, static_folder=STATIC_FOLDER, static_url_path="/static")
def should_auto_sync_flex(status: dict) -> bool:
if not status.get("configured"):
return False
latest_timestamp = status.get("latest_timestamp")
if latest_timestamp is None:
return True
return (time.time() - latest_timestamp) > FLEX_AUTO_SYNC_MAX_AGE_SECONDS
def get_flex_status_with_sync_state() -> dict:
status = get_flex_status()
status.update(_flex_sync_state)
return status
def run_flex_sync_if_needed(reason: str) -> bool:
status = get_flex_status()
if not should_auto_sync_flex(status):
return False
if not _flex_sync_lock.acquire(blocking=False):
return False
try:
_flex_sync_state["last_attempt_at"] = time.time()
_flex_sync_state["last_trigger"] = reason
_flex_sync_state["last_result"] = "running"
_flex_sync_state["last_error"] = None
# Re-check after taking the lock so concurrent callers do not duplicate work.
status = get_flex_status()
if not should_auto_sync_flex(status):
_flex_sync_state["last_result"] = "skipped"
return False
result = fetch_flex_statement()
_flex_sync_state["last_success_at"] = time.time()
_flex_sync_state["last_result"] = "success"
_flex_sync_state["last_saved_to"] = result["saved_to"]
_flex_sync_state["last_reference_code"] = result["reference_code"]
print(
f"Flex sync completed ({reason}): saved_to={result['saved_to']} "
f"reference_code={result['reference_code']}"
)
return True
except FlexQueryError as exc:
_flex_sync_state["last_result"] = "error"
_flex_sync_state["last_error"] = str(exc)
print(f"Flex sync skipped/failed ({reason}): {exc}")
return False
except Exception as exc:
_flex_sync_state["last_result"] = "error"
_flex_sync_state["last_error"] = str(exc)
print(f"Unexpected Flex sync failure ({reason}): {exc}")
return False
finally:
_flex_sync_lock.release()
def start_flex_startup_sync_if_needed() -> None:
global _flex_startup_sync_started
if _flex_startup_sync_started:
return
_flex_startup_sync_started = True
thread = threading.Thread(
target=run_flex_sync_if_needed,
args=("startup",),
daemon=True,
)
thread.start()
# instantiate the IB client with a process-unique client id to avoid
# collisions when Flask's reloader spawns multiple processes.
ib = IBClient(client_id=os.getpid())
# Start connection only in the main process (Werkzeug sets WERKZEUG_RUN_MAIN)
if os.environ.get("WERKZEUG_RUN_MAIN") == "true" or not os.environ.get("WERKZEUG_RUN_MAIN"):
# attempt to start the connection; failures are logged by the client
try:
ib.connect_and_start()
except Exception:
pass
try:
ib.connect_and_start()
except Exception:
pass
start_flex_startup_sync_if_needed()
@app.route("/")
@@ -216,7 +303,7 @@ def api_executions_debug():
@app.route("/api/flex/status")
def api_flex_status():
return jsonify(get_flex_status())
return jsonify(get_flex_status_with_sync_state())
@app.route("/api/flex/lots-debug")
@@ -236,11 +323,23 @@ def api_flex_lots_debug():
@app.route("/api/flex/sync", methods=["POST"])
def api_flex_sync():
try:
_flex_sync_state["last_attempt_at"] = time.time()
_flex_sync_state["last_trigger"] = "manual"
_flex_sync_state["last_result"] = "running"
_flex_sync_state["last_error"] = None
result = fetch_flex_statement()
_flex_sync_state["last_success_at"] = time.time()
_flex_sync_state["last_result"] = "success"
_flex_sync_state["last_saved_to"] = result["saved_to"]
_flex_sync_state["last_reference_code"] = result["reference_code"]
return jsonify({"ok": True, **result})
except FlexQueryError as exc:
_flex_sync_state["last_result"] = "error"
_flex_sync_state["last_error"] = str(exc)
return jsonify({"ok": False, "error": str(exc)}), 400
except Exception as exc:
_flex_sync_state["last_result"] = "error"
_flex_sync_state["last_error"] = str(exc)
return jsonify({"ok": False, "error": str(exc)}), 500