agharsallah Codex commited on
Commit
7eccfb0
·
1 Parent(s): b9afb6f

fix: auto-load .env (Python) + normalize Neon postgres scheme for live mode

Browse files

The 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>

Files changed (2) hide show
  1. app.py +44 -2
  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
- from src.ui.fishbowl.app import demo, launch
 
 
11
 
12
- __all__ = ["demo", "launch"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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))