agharsallah Codex commited on
Commit ·
7eccfb0
1
Parent(s): b9afb6f
fix: auto-load .env (Python) + normalize Neon postgres scheme for live mode
Browse filesThe app read os.environ directly, so 'uv run app.py' ran offline unless the user
sourced .env first — and 'source .env' aborts on a Neon URL's '&' (and JSON values),
so DATABASE_URL/MEMORY_INDEX/MEM0_API_KEY never reached the process. Load .env in
Python at startup (real env wins via setdefault; empty values skipped; skipped under
pytest so the suite stays hermetically offline). Also normalize bare postgresql:// and
postgres:// to postgresql+psycopg:// so a copy-pasted Neon URL uses the shipped psycopg3
driver instead of failing on the absent psycopg2.
Co-Authored-By: Codex <codex@openai.com>
- app.py +44 -2
- src/core/ledger_factory.py +17 -1
app.py
CHANGED
|
@@ -7,9 +7,51 @@ stub drives the cast so the demo is reproducible on stage with no credentials.
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
if __name__ == "__main__":
|
| 15 |
launch()
|
|
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
from pathlib import Path
|
| 13 |
|
| 14 |
+
|
| 15 |
+
def _load_dotenv(filename: str = ".env") -> None:
|
| 16 |
+
"""Load ``KEY=VALUE`` pairs from a sibling ``.env`` into the environment, if present.
|
| 17 |
+
|
| 18 |
+
Real environment variables win (``setdefault``) — this only fills gaps, never
|
| 19 |
+
overrides what the shell or CI already set. Parsed in Python, not the shell, so
|
| 20 |
+
values with shell-special characters load correctly: a Neon ``DATABASE_URL``'s
|
| 21 |
+
``&``, a JSON ``MEMORY_INDEX_CONFIG`` — all of which silently abort ``source .env``.
|
| 22 |
+
Absent ``.env`` → no-op, preserving the offline-by-default behaviour (no keys, stub).
|
| 23 |
+
"""
|
| 24 |
+
path = Path(__file__).resolve().parent / filename
|
| 25 |
+
if not path.exists():
|
| 26 |
+
return
|
| 27 |
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
| 28 |
+
line = raw.strip()
|
| 29 |
+
if not line or line.startswith("#"):
|
| 30 |
+
continue
|
| 31 |
+
if line.startswith("export "):
|
| 32 |
+
line = line[len("export ") :]
|
| 33 |
+
key, sep, value = line.partition("=")
|
| 34 |
+
if not sep:
|
| 35 |
+
continue
|
| 36 |
+
key, value = key.strip(), value.strip()
|
| 37 |
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
| 38 |
+
value = value[1:-1] # strip matching surrounding quotes
|
| 39 |
+
# Skip empty values: a blank `KEY=` in .env means "not configured", and
|
| 40 |
+
# setting it to "" would shadow `os.getenv(KEY, default)` callers (e.g.
|
| 41 |
+
# Gradio's GRADIO_SERVER_PORT) that expect absent → default.
|
| 42 |
+
if key and value:
|
| 43 |
+
os.environ.setdefault(key, value)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# Load .env BEFORE importing the app: the registry/router/ledger/memory all read the
|
| 47 |
+
# environment at import and on Summon, so credentials must be present first. Skipped
|
| 48 |
+
# under pytest so the test suite stays hermetically offline (no .env bleed into tests).
|
| 49 |
+
if "pytest" not in sys.modules:
|
| 50 |
+
_load_dotenv()
|
| 51 |
+
|
| 52 |
+
from src.ui.fishbowl.app import demo, launch # noqa: E402 (must follow _load_dotenv)
|
| 53 |
+
|
| 54 |
+
__all__ = ["demo", "launch", "_load_dotenv"]
|
| 55 |
|
| 56 |
if __name__ == "__main__":
|
| 57 |
launch()
|
src/core/ledger_factory.py
CHANGED
|
@@ -11,6 +11,7 @@ durably stored.
|
|
| 11 |
With no ``DATABASE_URL`` the system never imports SQLAlchemy or a database driver,
|
| 12 |
so the offline path stays import-clean and fully testable without a server.
|
| 13 |
"""
|
|
|
|
| 14 |
from __future__ import annotations
|
| 15 |
|
| 16 |
import os
|
|
@@ -24,6 +25,21 @@ def database_url() -> str | None:
|
|
| 24 |
return url or None
|
| 25 |
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
def make_ledger(url: str | None = None) -> Ledger:
|
| 28 |
"""Construct the configured ledger backend.
|
| 29 |
|
|
@@ -36,4 +52,4 @@ def make_ledger(url: str | None = None) -> Ledger:
|
|
| 36 |
return Ledger()
|
| 37 |
from src.core.sqlalchemy_ledger import SqlAlchemyLedger
|
| 38 |
|
| 39 |
-
return SqlAlchemyLedger(resolved)
|
|
|
|
| 11 |
With no ``DATABASE_URL`` the system never imports SQLAlchemy or a database driver,
|
| 12 |
so the offline path stays import-clean and fully testable without a server.
|
| 13 |
"""
|
| 14 |
+
|
| 15 |
from __future__ import annotations
|
| 16 |
|
| 17 |
import os
|
|
|
|
| 25 |
return url or None
|
| 26 |
|
| 27 |
|
| 28 |
+
def _normalize_db_url(url: str) -> str:
|
| 29 |
+
"""Steer a bare Postgres URL to the installed psycopg3 driver.
|
| 30 |
+
|
| 31 |
+
Neon (and most providers) hand out ``postgresql://`` / ``postgres://``, which
|
| 32 |
+
SQLAlchemy maps to psycopg2 — but this project ships psycopg3 (the ``store``
|
| 33 |
+
extra), so a copy-pasted Neon URL would fail with a missing-driver error.
|
| 34 |
+
Rewrite the bare scheme to ``postgresql+psycopg://``; URLs that already name a
|
| 35 |
+
driver (``postgresql+...``) or use another backend (sqlite, …) pass through.
|
| 36 |
+
"""
|
| 37 |
+
for scheme in ("postgresql://", "postgres://"):
|
| 38 |
+
if url.startswith(scheme):
|
| 39 |
+
return "postgresql+psycopg://" + url[len(scheme) :]
|
| 40 |
+
return url
|
| 41 |
+
|
| 42 |
+
|
| 43 |
def make_ledger(url: str | None = None) -> Ledger:
|
| 44 |
"""Construct the configured ledger backend.
|
| 45 |
|
|
|
|
| 52 |
return Ledger()
|
| 53 |
from src.core.sqlalchemy_ledger import SqlAlchemyLedger
|
| 54 |
|
| 55 |
+
return SqlAlchemyLedger(_normalize_db_url(resolved))
|