Skip to content

Commit 8bedd8d

Browse files
author
Presta
committed
feat(37): add MODEL_DIR env override -- enable isolated instances per project (51-ACA)
- config.py: model_dir_override field with validation_alias=MODEL_DIR + populate_by_name=True - admin.py: _MODEL_DIR constant -> _get_model_dir() function (3 call sites) - server.py: import and path references updated to _get_model_dir(); utf-8-sig for BOM resilience - .env.example: MODEL_DIR documentation added - STATUS.md: session 17c header
1 parent 6a1fdcb commit 8bedd8d

File tree

5 files changed

+34
-15
lines changed

5 files changed

+34
-15
lines changed

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ COSMOS_KEY=
55
MODEL_DB_NAME=evamodel
66
MODEL_CONTAINER_NAME=model_objects
77

8+
# ── Model Data Directory ──────────────────────────────────────────────────────
9+
# Leave blank to use the default model/ folder beside this server's code.
10+
# Set to an absolute path to run an isolated instance for a different project
11+
# (e.g. MODEL_DIR=C:\myproject\data-model\model). Used by 51-ACA side project.
12+
MODEL_DIR=
13+
814
# ── Redis ─────────────────────────────────────────────────────────────────────
915
# Leave blank to use in-process memory cache
1016
REDIS_URL=

STATUS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# EVA Data Model -- Status
22

3-
**Last Updated:** February 26, 2026 -- Session 17b: evidence receipts for 25 API stories -- MTI 86 -> 100 -- all 10 readiness gates PASS
3+
**Last Updated:** February 26, 2026 -- Session 17c: MODEL_DIR override + 51-ACA isolated instance -- services=4 endpoints=14 containers=5 agents=4 total=27
44
**Phase:** ACTIVE -- COSMOS 24x7 -- validate-model PASS 0 violations -- 31 layers registered -- MTI=100
5-
**Snapshot (2026-02-26 S17b):** 31 layers -- 4057 exported objects -- Tests: 41/42 (T36 pre-existing only) -- Readiness: 10/10 gates PASS (G09 PASS: MTI=100 delta=+14)
5+
**Snapshot (2026-02-26 S17c):** 31 layers -- 4057 exported objects -- Tests: 41/42 (T36 pre-existing only) -- Readiness: 10/10 gates PASS (G09 PASS: MTI=100) -- MODEL_DIR env override: enables isolated per-project instances (e.g. port 8011 for 51-ACA)
66

77
> **Session note (2026-02-26 Session 17 -- brain-v2 housekeeping + evidence batch):**
88
>

api/config.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"""
2-
Configuration loaded from environment variables.
2+
Configuration -- loaded from environment variables.
33
All settings have defaults so the API works with zero config (in-memory mode).
44
"""
55
from __future__ import annotations
6+
from pydantic import Field
67
from pydantic_settings import BaseSettings, SettingsConfigDict
78

89

910
class Settings(BaseSettings):
10-
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
11+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore", populate_by_name=True)
1112

1213
# --- Cosmos DB (optional — if not set, MemoryStore is used) ---
1314
cosmos_url: str = ""
@@ -33,10 +34,16 @@ class Settings(BaseSettings):
3334
# Set DEV_MODE=false in production .env or container environment.
3435
dev_mode: bool = True
3536

36-
# Path to the model directory (resolved relative to this file's parent)
37+
# Path to the model directory.
38+
# Override with MODEL_DIR env var to run an isolated instance pointing at a
39+
# different model data folder (e.g. a side project with its own layer JSON files).
40+
model_dir_override: str = Field(default="", validation_alias="MODEL_DIR")
41+
3742
@property
3843
def model_dir(self) -> str:
3944
from pathlib import Path
45+
if self.model_dir_override:
46+
return str(Path(self.model_dir_override))
4047
return str(Path(__file__).parents[1] / "model")
4148

4249
@property

api/routers/admin.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,13 @@
8888
"decisions": "decisions.json",
8989
}
9090

91-
_MODEL_DIR = Path(__file__).parents[2] / "model"
91+
def _get_model_dir() -> Path:
92+
"""Return the model data directory, honouring MODEL_DIR env override."""
93+
from api.config import get_settings
94+
override = get_settings().model_dir_override
95+
if override:
96+
return Path(override)
97+
return Path(__file__).parents[2] / "model"
9298

9399

94100
# ── SEED ──────────────────────────────────────────────────────────────────────
@@ -112,7 +118,7 @@ async def seed(
112118
errors: list[str] = []
113119

114120
for layer, filename in _LAYER_FILES.items():
115-
path = _MODEL_DIR / filename
121+
path = _get_model_dir() / filename
116122
if not path.exists():
117123
log.warning("Seed: %s not found, skipping", path)
118124
continue
@@ -185,7 +191,7 @@ async def export_to_disk(
185191
_STRIP = {"obj_id", "layer", "_rid", "_self", "_etag", "_attachments", "_ts"}
186192

187193
for layer, filename in _LAYER_FILES.items():
188-
path = _MODEL_DIR / filename
194+
path = _get_model_dir() / filename
189195
try:
190196
objects = await store.get_all(layer, active_only=False)
191197
except Exception as exc:
@@ -526,7 +532,7 @@ async def commit(
526532
_STRIP = {"obj_id", "layer", "_rid", "_self", "_etag", "_attachments", "_ts"}
527533

528534
for layer, filename in _LAYER_FILES.items():
529-
path = _MODEL_DIR / filename
535+
path = _get_model_dir() / filename
530536
try:
531537
objects = await store.get_all(layer, active_only=False)
532538
except Exception as exc:

api/server.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,14 @@ async def lifespan(app: FastAPI):
126126
# object so every record permanently knows which layer file it came from.
127127
from api.store.memory import MemoryStore as _MS
128128
if isinstance(store, _MS):
129-
from api.routers.admin import _LAYER_FILES, _MODEL_DIR
129+
from api.routers.admin import _LAYER_FILES, _get_model_dir
130130
import json
131131
total = 0
132132
for layer, filename in _LAYER_FILES.items():
133-
path = _MODEL_DIR / filename
133+
path = _get_model_dir() / filename
134134
if not path.exists():
135135
continue
136-
raw = json.loads(path.read_text(encoding="utf-8"))
136+
raw = json.loads(path.read_text(encoding="utf-8-sig"))
137137
objects = raw.get(layer, [])
138138
if not objects:
139139
for v in raw.values():
@@ -163,19 +163,19 @@ async def lifespan(app: FastAPI):
163163
log.info("Shutdown: exporting MemoryStore to disk JSON layer files...")
164164
try:
165165
import json as _json
166-
from api.routers.admin import _LAYER_FILES, _MODEL_DIR
166+
from api.routers.admin import _LAYER_FILES, _get_model_dir
167167
_STRIP = {"obj_id", "layer", "_rid", "_self", "_etag", "_attachments", "_ts"}
168168
_total = 0
169169
for _layer, _filename in _LAYER_FILES.items():
170-
_path = _MODEL_DIR / _filename
170+
_path = _get_model_dir() / _filename
171171
try:
172172
_objects = await store.get_all(_layer, active_only=False)
173173
except Exception:
174174
continue
175175
_schema_url = ""
176176
if _path.exists():
177177
try:
178-
_existing = _json.loads(_path.read_text(encoding="utf-8"))
178+
_existing = _json.loads(_path.read_text(encoding="utf-8-sig"))
179179
_schema_url = _existing.get("$schema", "")
180180
except Exception:
181181
pass

0 commit comments

Comments
 (0)