Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions app/backend/api/services/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,30 @@
# ******************************************************************************
import os


def prepareEnvironment():
"""Prepare the Scipion environment with all variables"""
from scipion.__main__ import Vars

variables = Vars.init()
os.environ.update(variables)
# Trigger Config initialization once environment is ready

import pyworkflow
pwVARS = pyworkflow.Config.getVars()
variables.update(pwVARS)

pyworkflow.Config.setDomain("pwem")
pyworkflow.Config.getDomain()
# Update the environment now with pyworkflow values.
domain = pyworkflow.Config.getDomain()

# Force protocol registry loading so VariablesRegistry is complete
domain.getProtocols()

pwVars = pyworkflow.Config.getVars()

# Load config-derived values first
os.environ.update(pwVars)

# Keep backend bootstrap values as priority
os.environ.update(variables)
os.chdir(variables.get(pyworkflow.SCIPION_HOME_VAR))

scipionHome = variables.get(pyworkflow.SCIPION_HOME_VAR) or os.environ.get(pyworkflow.SCIPION_HOME_VAR)
if scipionHome:
os.chdir(scipionHome)
131 changes: 115 additions & 16 deletions app/backend/api/services/settings_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,28 +524,94 @@ def patchInstanceSettings(
stored = mapper.upsertInstanceSettings(_modelDump(normalized))
return _modelValidate(InstanceSettingsOut, stored or {})

def _registryCount(self) -> int:
# registryCount
try:
return len(list(VariablesRegistry.__iter__()))
except Exception:
return -1

def _warmupEnvironmentRegistry(self) -> None:
# warmupEnvironmentRegistry
from scipion.__main__ import Vars
from pyworkflow.project import Manager
from pyworkflow.template import TemplateList

logger.warning("registry count [start]: %s", self._registryCount())

Vars.init()
logger.warning("registry count [after Vars.init]: %s", self._registryCount())

pyworkflow.Config.getVars()
logger.warning("registry count [after Config.getVars]: %s", self._registryCount())

pyworkflow.Config.setDomain("pwem")
domain = pyworkflow.Config.getDomain()
logger.warning("registry count [after Config.getDomain]: %s", self._registryCount())

try:
domain.getProtocols()
except Exception:
logger.exception("Error warming domain.getProtocols()")
logger.warning("registry count [after domain.getProtocols]: %s", self._registryCount())

try:
Manager()
except Exception:
logger.exception("Error warming Manager()")
logger.warning("registry count [after Manager()]: %s", self._registryCount())

try:
tempList = TemplateList()
tempList.addScipionTemplates(None)
tempList.addPluginTemplates(None)
except Exception:
logger.exception("Error warming TemplateList plugins")
logger.warning("registry count [after TemplateList warmup]: %s", self._registryCount())

def getEnvironmentVariables(self, currentUser: Any) -> list[dict[str, str]]:
# getEnvironmentVariables
self._requireAdmin(currentUser)

with _envLock:
self._warmupEnvironmentRegistry()

rows = []
for v in VariablesRegistry.__iter__():

for variable in VariablesRegistry.__iter__():
try:
variableName = _toStr(getattr(variable, "name", "")).strip()
if not variableName:
continue

currentValue = getattr(variable, "value", None)
if variableName in os.environ:
currentValue = os.environ.get(variableName, "")
variable.value = currentValue

defaultValue = getattr(variable, "default", None)
if defaultValue is not None:
variable.isDefault = str(defaultValue) == str(currentValue)

rows.append(
{
"name": str(v.name),
"value": "" if v is None else str(v.value),
"default": "" if v.default is None else str(v.default),
"description": "" if v.description is None else str(v.description),
"source": "" if v.source is None else str(v.source),
"isDefault": "" if v.isDefault is None else v.isDefault,
"type": "STRING" if v.var_type is None else str(v.var_type.name),
"name": variableName,
"value": "" if currentValue is None else str(currentValue),
"default": "" if getattr(variable, "default", None) is None else str(variable.default),
"description": "" if getattr(variable, "description", None) is None else str(
variable.description),
"source": "" if getattr(variable, "source", None) is None else str(variable.source),
"isDefault": False if getattr(variable, "isDefault", None) is None else bool(
variable.isDefault),
"type": "STRING"
if getattr(variable, "var_type", None) is None
else str(variable.var_type.name),
}
)
except Exception:
print(v.name)
continue

rows.sort(key=lambda x: (x.get("name") or "").upper())
rows.sort(key=lambda item: (item.get("name") or "").upper())
return rows

def patchEnvironmentVariables(self, currentUser: Any, patch: Dict[str, Any]) -> list[dict[str, str]]:
Expand All @@ -558,13 +624,46 @@ def patchEnvironmentVariables(self, currentUser: Any, patch: Dict[str, Any]) ->
detail="Invalid payload: expected an object mapping variable names to values.",
)

for v in VariablesRegistry.__iter__():
if v.name in patch:
v.value = patch[v.name]
v.isDefault = False
changed = False

with _envLock:
patchItems: dict[str, str] = {}
for rawName, rawValue in patch.items():
name = _toStr(rawName).strip()
if not name:
continue
patchItems[name] = "" if rawValue is None else str(rawValue)

if patchItems:
for variable in VariablesRegistry.__iter__():
try:
variableName = _toStr(getattr(variable, "name", "")).strip()
if not variableName or variableName not in patchItems:
continue

nextValue = patchItems[variableName]
currentValue = "" if getattr(variable, "value", None) is None else str(variable.value)

if currentValue == nextValue and os.environ.get(variableName) == nextValue:
continue

os.environ[variableName] = nextValue
variable.value = nextValue

defaultValue = getattr(variable, "default", None)
if defaultValue is not None:
variable.isDefault = str(defaultValue) == str(nextValue)
else:
variable.isDefault = False

changed = True
except Exception:
continue

if changed:
VariablesRegistry.save(pyworkflow.Config.SCIPION_CONFIG)

VariablesRegistry.save(pyworkflow.Config.SCIPION_CONFIG)
if patch:
if changed:
triggerBackendReloadIfEnabled()

return self.getEnvironmentVariables(currentUser)
Expand Down
48 changes: 31 additions & 17 deletions app/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@
from pathlib import Path

from app.backend.bootstrap import bootstrapEnv
from app.backend.api.services.environment import prepareEnvironment

# bootstrapEnvFirst
bootstrapEnv()

# prepareScipionEnvironmentBeforeImportingRouters
prepareEnvironment()

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

Expand All @@ -40,7 +44,6 @@
from app.backend.api.routers.auth_router import router as auth
from app.backend.api.routers.user_router import router as users
from app.backend.api.routers.settings_router import router as settingsRouter
from app.backend.api.services.environment import prepareEnvironment
from app.backend.utils.error_handlers import registerAllErrorHandlers
from starlette.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as StarletteHttpException
Expand All @@ -63,18 +66,24 @@ async def get_response(self, path: str, scope):

def _buildApiApp() -> FastAPI:
# buildApiApp
apiApp = FastAPI(title="Scipion API",
debug=True,
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",)
apiApp = FastAPI(
title="Scipion API",
debug=True,
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)

# registerCustomErrorHandlers
registerAllErrorHandlers(apiApp)

apiApp.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://localhost:5174", "http://127.0.0.1:5173",],
allow_origins=[
"http://localhost:5173",
"http://localhost:5174",
"http://127.0.0.1:5173",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=[
Expand All @@ -88,7 +97,7 @@ def _buildApiApp() -> FastAPI:
"Scipion-Colormap",
"Colormap",
"X-Preview-Schema",
"X-Preview-Name"
"X-Preview-Name",
],
expose_headers=[
"Content-Disposition",
Expand All @@ -109,9 +118,6 @@ def _buildApiApp() -> FastAPI:
],
)

# prepareScipionEnvironment
prepareEnvironment()

# includeRouters
apiApp.include_router(projects)
apiApp.include_router(protocols)
Expand Down Expand Up @@ -158,16 +164,25 @@ def _resolveWebDistPath() -> Path:
apiMountPath = _normalizeMountPath(os.getenv("API_MOUNT_PATH") or "/api")

# alwaysMountApiUnderApiMountPath
app = FastAPI(title="Scipion Web", debug=True, docs_url=None,
redoc_url=None,
openapi_url=None,)
app = FastAPI(
title="Scipion Web",
debug=True,
docs_url=None,
redoc_url=None,
openapi_url=None,
)

app.mount(apiMountPath, apiApp)


@app.get("/health", include_in_schema=False)
def health_check():
# healthCheckRoot
return {"status": "ok", "mode": "api-only" if not serveWeb else "combined", "apiMountPath": apiMountPath}
return {
"status": "ok",
"mode": "api-only" if not serveWeb else "combined",
"apiMountPath": apiMountPath,
}


# optional: keepConvenienceRedirects
Expand All @@ -187,5 +202,4 @@ def openapi_redirect():

if serveWeb and webDistPath and (webDistPath / "index.html").exists():
# mountSpaStaticRootLast
app.mount("/", SpaStaticFiles(directory=str(webDistPath), html=True), name="web")

app.mount("/", SpaStaticFiles(directory=str(webDistPath), html=True), name="web")
Loading