gateway/modules/features/realEstate/interfaceFeatureRealEstate.py
ValueOn AG df4c60fc99 fixes
2026-01-24 18:01:28 +01:00

840 lines
32 KiB
Python

"""
Interface to Real Estate database objects.
Uses PostgreSQL connector for data access with user/mandate filtering.
Handles CRUD operations on Real Estate entities (Projekt, Parzelle, etc.).
"""
import logging
from typing import Dict, Any, List, Optional, Union
from .datamodelFeatureRealEstate import (
Projekt,
Parzelle,
Dokument,
Kanton,
Gemeinde,
Land,
GeoPolylinie,
GeoPunkt,
Kontext,
StatusProzess,
)
from modules.datamodels.datamodelUam import User
from modules.connectors.connectorDbPostgre import DatabaseConnector
from modules.shared.configuration import APP_CONFIG
from modules.security.rbac import RbacClass
from modules.datamodels.datamodelRbac import AccessRuleContext
from modules.datamodels.datamodelUam import AccessLevel
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
logger = logging.getLogger(__name__)
# Singleton factory for Real Estate interfaces
_realEstateInterfaces = {}
class RealEstateObjects:
"""
Interface to Real Estate database objects.
Uses PostgreSQL connector for data access with user/mandate filtering.
Handles CRUD operations on Real Estate entities.
"""
def __init__(self, currentUser: Optional[User] = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None):
"""Initializes the Real Estate Interface.
Args:
currentUser: The authenticated user
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header)
"""
self.currentUser = currentUser
self.userId = currentUser.id if currentUser else None
# Use mandateId from parameter (Request-Context), not from user object
self.mandateId = mandateId
self.featureInstanceId = featureInstanceId
self.rbac = None # RBAC interface
# Initialize database
self._initializeDatabase()
# Set user context if provided
if currentUser:
self.setUserContext(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId)
def _initializeDatabase(self):
"""Initialize PostgreSQL database connection."""
try:
# Get database configuration from environment
dbHost = APP_CONFIG.get("DB_HOST", "_no_config_default_data")
dbDatabase = "poweron_realestate"
dbUser = APP_CONFIG.get("DB_USER")
dbPassword = APP_CONFIG.get("DB_PASSWORD_SECRET")
dbPort = int(APP_CONFIG.get("DB_PORT", 5432))
# Initialize database connector
self.db = DatabaseConnector(
dbHost=dbHost,
dbDatabase=dbDatabase,
dbUser=dbUser,
dbPassword=dbPassword,
dbPort=dbPort,
userId=self.userId if self.userId else None,
)
# Initialize database system (creates database and system table if needed)
# Note: This is also called in DatabaseConnector.__init__, but we call it explicitly
# for consistency with other interfaces and to ensure proper initialization
self.db.initDbSystem()
# Ensure all supporting tables are created (Land, Kanton, Gemeinde, Dokument)
# These tables are needed for foreign key relationships
self._ensureSupportingTablesExist()
logger.info(f"Real Estate database connector initialized for database: {dbDatabase}")
except Exception as e:
logger.error(f"Error initializing Real Estate database: {e}")
raise
def _ensureSupportingTablesExist(self):
"""Ensure all supporting tables (Land, Kanton, Gemeinde, Dokument) are created."""
try:
# These tables are created on-demand when first accessed, but we ensure they exist here
# to avoid errors when resolving location names to IDs
self.db._ensureTableExists(Land)
self.db._ensureTableExists(Kanton)
self.db._ensureTableExists(Gemeinde)
self.db._ensureTableExists(Dokument)
logger.debug("Supporting tables (Land, Kanton, Gemeinde, Dokument) verified/created")
except Exception as e:
logger.warning(f"Error ensuring supporting tables exist: {e}")
# Don't raise - tables will be created on-demand anyway
def setUserContext(self, currentUser: User, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None):
"""Sets the user context for the interface.
Args:
currentUser: The authenticated user
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header)
"""
self.currentUser = currentUser
self.userId = currentUser.id
# Use mandateId from parameter (Request-Context), not from user object
self.mandateId = mandateId
self.featureInstanceId = featureInstanceId
if not self.userId:
raise ValueError("Invalid user context: id is required")
# Note: mandateId is ALWAYS optional here - it comes from Request-Context, not from User.
# Users are NOT assigned to mandates by design - they get mandate context from the request.
# sysAdmin users can additionally perform cross-mandate operations.
# Without mandateId, operations will be filtered to accessible mandates via RBAC.
# Initialize RBAC interface
if not self.currentUser:
raise ValueError("User context is required for RBAC")
# Get DbApp connection for RBAC AccessRule queries
from modules.security.rootAccess import getRootDbAppConnector
dbApp = getRootDbAppConnector()
self.rbac = RbacClass(self.db, dbApp=dbApp)
# Update database context
self.db.updateContext(self.userId)
# ===== Projekt Methods =====
def createProjekt(self, projekt: Projekt) -> Projekt:
"""Create a new project."""
# Check RBAC permission
if not self.checkRbacPermission(Projekt, "create"):
raise PermissionError(f"User {self.userId} cannot create projects")
# Ensure mandateId and featureInstanceId are set for proper data isolation
if not projekt.mandateId:
projekt.mandateId = self.mandateId
if not projekt.featureInstanceId:
projekt.featureInstanceId = self.featureInstanceId
# Save to database - use mode='json' to ensure nested Pydantic models are serialized
self.db.recordCreate(Projekt, projekt.model_dump(mode='json'))
return projekt
def getProjekt(self, projektId: str) -> Optional[Projekt]:
"""Get a project by ID."""
records = getRecordsetWithRBAC(
self.db,
Projekt,
self.currentUser,
recordFilter={"id": projektId}
)
if not records:
return None
return Projekt(**records[0])
def getProjekte(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Projekt]:
"""Get all projects matching the filter."""
records = getRecordsetWithRBAC(
self.db,
Projekt,
self.currentUser,
recordFilter=recordFilter or {}
)
return [Projekt(**r) for r in records]
def updateProjekt(self, projektId_or_projekt: Union[str, Projekt], updateData: Optional[Dict[str, Any]] = None) -> Optional[Projekt]:
"""Update a project.
Args:
projektId_or_projekt: Either a project ID (str) or a Projekt object
updateData: Optional dict of fields to update (only used when projektId_or_projekt is a string)
"""
# Handle both Projekt object and projektId string
if isinstance(projektId_or_projekt, Projekt):
projekt = projektId_or_projekt
projektId = projekt.id
else:
projektId = projektId_or_projekt
projekt = self.getProjekt(projektId)
if not projekt:
return None
# Update fields from updateData if provided
if updateData:
for key, value in updateData.items():
if hasattr(projekt, key):
setattr(projekt, key, value)
# Check RBAC permission
if not self.checkRbacPermission(Projekt, "update", projektId):
raise PermissionError(f"User {self.userId} cannot modify project {projektId}")
# Save to database
self.db.recordModify(Projekt, projektId, projekt.model_dump())
return projekt
def deleteProjekt(self, projektId: str) -> bool:
"""Delete a project."""
projekt = self.getProjekt(projektId)
if not projekt:
return False
# Check RBAC permission
if not self.checkRbacPermission(Projekt, "delete", projektId):
raise PermissionError(f"User {self.userId} cannot delete project {projektId}")
return self.db.recordDelete(Projekt, projektId)
# ===== Parzelle Methods =====
def createParzelle(self, parzelle: Parzelle) -> Parzelle:
"""Create a new plot."""
# Check RBAC permission
if not self.checkRbacPermission(Parzelle, "create"):
raise PermissionError(f"User {self.userId} cannot create plots")
# Ensure mandateId and featureInstanceId are set for proper data isolation
if not parzelle.mandateId:
parzelle.mandateId = self.mandateId
if not parzelle.featureInstanceId:
parzelle.featureInstanceId = self.featureInstanceId
# Use mode='json' to ensure nested Pydantic models (like GeoPolylinie) are serialized
self.db.recordCreate(Parzelle, parzelle.model_dump(mode='json'))
return parzelle
def getParzelle(self, parzelleId: str) -> Optional[Parzelle]:
"""Get a plot by ID."""
records = getRecordsetWithRBAC(
self.db,
Parzelle,
self.currentUser,
recordFilter={"id": parzelleId}
)
if not records:
return None
return Parzelle(**records[0])
def getParzellen(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Parzelle]:
"""Get all plots matching the filter."""
# Resolve location names to IDs if needed
if recordFilter:
recordFilter = self._resolveLocationFilters(recordFilter)
records = getRecordsetWithRBAC(
self.db,
Parzelle,
self.currentUser,
recordFilter=recordFilter or {}
)
return [Parzelle(**r) for r in records]
def _resolveLocationFilters(self, recordFilter: Dict[str, Any]) -> Dict[str, Any]:
"""
Resolve location names to IDs for foreign key fields.
Only handles kontextGemeinde (Parzelle → Gemeinde).
Note: Parzelle does NOT have direct links to Kanton or Land.
The relationship is: Parzelle → Gemeinde → Kanton → Land
"""
resolvedFilter = recordFilter.copy()
# Resolve Gemeinde name to ID
# This is the only direct location link on Parzelle
if "kontextGemeinde" in resolvedFilter:
gemeindeValue = resolvedFilter["kontextGemeinde"]
# Check if it's a name (not a UUID-like string)
if not self._isUUID(gemeindeValue):
gemeindeId = self._resolveGemeindeByName(gemeindeValue)
if gemeindeId:
resolvedFilter["kontextGemeinde"] = gemeindeId
logger.debug(f"Resolved Gemeinde name '{gemeindeValue}' to ID '{gemeindeId}'")
else:
logger.warning(f"Gemeinde '{gemeindeValue}' not found, filter may return no results")
# Keep the original value - query will return empty if not found
# Note: kontextKanton and kontextLand are NOT fields on Parzelle
# If they appear in the filter, they will be filtered out by the validation in mainRealEstate.py
return resolvedFilter
def _isUUID(self, value: str) -> bool:
"""Check if a string looks like a UUID."""
import re
uuid_pattern = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.IGNORECASE)
return bool(uuid_pattern.match(value))
def _resolveGemeindeByName(self, name: str) -> Optional[str]:
"""Resolve Gemeinde name to ID by looking up in Gemeinde table."""
try:
# First try exact match
gemeinden = self.db.getRecordset(
Gemeinde,
recordFilter={"label": name}
)
if gemeinden:
gemeindeId = gemeinden[0].get("id")
logger.debug(f"Found Gemeinde '{name}' with ID '{gemeindeId}'")
return gemeindeId
# If no exact match, try case-insensitive search via SQL query
# This handles cases where the name might have different casing
self.db._ensure_connection()
with self.db.connection.cursor() as cursor:
cursor.execute(
'SELECT "id" FROM "Gemeinde" WHERE LOWER("label") = LOWER(%s) LIMIT 1',
(name,)
)
result = cursor.fetchone()
if result:
# psycopg2 returns tuples, so result[0] is the id
gemeindeId = result[0]
logger.debug(f"Found Gemeinde '{name}' (case-insensitive) with ID '{gemeindeId}'")
return gemeindeId
logger.warning(f"Gemeinde '{name}' not found in database")
return None
except Exception as e:
logger.error(f"Error resolving Gemeinde by name '{name}': {e}", exc_info=True)
return None
def _resolveKantonByName(self, name: str) -> Optional[str]:
"""Resolve Kanton name to ID by looking up in Kanton table."""
try:
# First try exact match
kantone = self.db.getRecordset(
Kanton,
recordFilter={"label": name}
)
if kantone:
kantonId = kantone[0].get("id")
logger.debug(f"Found Kanton '{name}' with ID '{kantonId}'")
return kantonId
# Try case-insensitive search
self.db._ensure_connection()
with self.db.connection.cursor() as cursor:
cursor.execute(
'SELECT "id" FROM "Kanton" WHERE LOWER("label") = LOWER(%s) LIMIT 1',
(name,)
)
result = cursor.fetchone()
if result:
# psycopg2 returns tuples, so result[0] is the id
kantonId = result[0]
logger.debug(f"Found Kanton '{name}' (case-insensitive) with ID '{kantonId}'")
return kantonId
logger.warning(f"Kanton '{name}' not found in database")
return None
except Exception as e:
logger.error(f"Error resolving Kanton by name '{name}': {e}", exc_info=True)
return None
def _resolveLandByName(self, name: str) -> Optional[str]:
"""Resolve Land name to ID by looking up in Land table."""
try:
# First try exact match
laender = self.db.getRecordset(
Land,
recordFilter={"label": name}
)
if laender:
landId = laender[0].get("id")
logger.debug(f"Found Land '{name}' with ID '{landId}'")
return landId
# Try case-insensitive search
self.db._ensure_connection()
with self.db.connection.cursor() as cursor:
cursor.execute(
'SELECT "id" FROM "Land" WHERE LOWER("label") = LOWER(%s) LIMIT 1',
(name,)
)
result = cursor.fetchone()
if result:
# psycopg2 returns tuples, so result[0] is the id
landId = result[0]
logger.debug(f"Found Land '{name}' (case-insensitive) with ID '{landId}'")
return landId
logger.warning(f"Land '{name}' not found in database")
return None
except Exception as e:
logger.error(f"Error resolving Land by name '{name}': {e}", exc_info=True)
return None
def updateParzelle(self, parzelleId: str, updateData: Dict[str, Any]) -> Optional[Parzelle]:
"""Update a plot."""
parzelle = self.getParzelle(parzelleId)
if not parzelle:
return None
if not self.checkRbacPermission(Parzelle, "update", parzelleId):
raise PermissionError(f"User {self.userId} cannot modify plot {parzelleId}")
for key, value in updateData.items():
if hasattr(parzelle, key):
setattr(parzelle, key, value)
self.db.recordModify(Parzelle, parzelleId, parzelle.model_dump())
return parzelle
def deleteParzelle(self, parzelleId: str) -> bool:
"""Delete a plot."""
parzelle = self.getParzelle(parzelleId)
if not parzelle:
return False
if not self.checkRbacPermission(Parzelle, "delete", parzelleId):
raise PermissionError(f"User {self.userId} cannot delete plot {parzelleId}")
return self.db.recordDelete(Parzelle, parzelleId)
# ===== Dokument Methods =====
def createDokument(self, dokument: Dokument) -> Dokument:
"""Create a new document."""
# Check RBAC permission
if not self.checkRbacPermission(Dokument, "create"):
raise PermissionError(f"User {self.userId} cannot create documents")
# Ensure mandateId and featureInstanceId are set for proper data isolation
if not dokument.mandateId:
dokument.mandateId = self.mandateId
if not dokument.featureInstanceId:
dokument.featureInstanceId = self.featureInstanceId
self.db.recordCreate(Dokument, dokument.model_dump())
return dokument
def getDokument(self, dokumentId: str) -> Optional[Dokument]:
"""Get a document by ID."""
records = getRecordsetWithRBAC(
self.db,
Dokument,
self.currentUser,
recordFilter={"id": dokumentId}
)
if not records:
return None
return Dokument(**records[0])
def getDokumente(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Dokument]:
"""Get all documents matching the filter."""
records = getRecordsetWithRBAC(
self.db,
Dokument,
self.currentUser,
recordFilter=recordFilter or {}
)
return [Dokument(**r) for r in records]
def updateDokument(self, dokumentId: str, updateData: Dict[str, Any]) -> Optional[Dokument]:
"""Update a document."""
dokument = self.getDokument(dokumentId)
if not dokument:
return None
if not self.checkRbacPermission(Dokument, "update", dokumentId):
raise PermissionError(f"User {self.userId} cannot modify document {dokumentId}")
for key, value in updateData.items():
if hasattr(dokument, key):
setattr(dokument, key, value)
self.db.recordModify(Dokument, dokumentId, dokument.model_dump())
return dokument
def deleteDokument(self, dokumentId: str) -> bool:
"""Delete a document."""
dokument = self.getDokument(dokumentId)
if not dokument:
return False
if not self.checkRbacPermission(Dokument, "delete", dokumentId):
raise PermissionError(f"User {self.userId} cannot delete document {dokumentId}")
return self.db.recordDelete(Dokument, dokumentId)
# ===== Gemeinde Methods =====
def createGemeinde(self, gemeinde: Gemeinde) -> Gemeinde:
"""Create a new municipality."""
# Check RBAC permission
if not self.checkRbacPermission(Gemeinde, "create"):
raise PermissionError(f"User {self.userId} cannot create municipalities")
# Ensure mandateId and featureInstanceId are set for proper data isolation
if not gemeinde.mandateId:
gemeinde.mandateId = self.mandateId
if not gemeinde.featureInstanceId:
gemeinde.featureInstanceId = self.featureInstanceId
self.db.recordCreate(Gemeinde, gemeinde.model_dump())
return gemeinde
def getGemeinde(self, gemeindeId: str) -> Optional[Gemeinde]:
"""Get a municipality by ID."""
records = getRecordsetWithRBAC(
self.db,
Gemeinde,
self.currentUser,
recordFilter={"id": gemeindeId}
)
if not records:
return None
return Gemeinde(**records[0])
def getGemeinden(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Gemeinde]:
"""Get all municipalities matching the filter."""
records = getRecordsetWithRBAC(
self.db,
Gemeinde,
self.currentUser,
recordFilter=recordFilter or {}
)
return [Gemeinde(**r) for r in records]
def updateGemeinde(self, gemeindeId: str, updateData: Dict[str, Any]) -> Optional[Gemeinde]:
"""Update a municipality."""
gemeinde = self.getGemeinde(gemeindeId)
if not gemeinde:
return None
if not self.checkRbacPermission(Gemeinde, "update", gemeindeId):
raise PermissionError(f"User {self.userId} cannot modify municipality {gemeindeId}")
for key, value in updateData.items():
if hasattr(gemeinde, key):
setattr(gemeinde, key, value)
self.db.recordModify(Gemeinde, gemeindeId, gemeinde.model_dump())
return gemeinde
def deleteGemeinde(self, gemeindeId: str) -> bool:
"""Delete a municipality."""
gemeinde = self.getGemeinde(gemeindeId)
if not gemeinde:
return False
if not self.checkRbacPermission(Gemeinde, "delete", gemeindeId):
raise PermissionError(f"User {self.userId} cannot delete municipality {gemeindeId}")
return self.db.recordDelete(Gemeinde, gemeindeId)
# ===== Kanton Methods =====
def createKanton(self, kanton: Kanton) -> Kanton:
"""Create a new canton."""
# Check RBAC permission
if not self.checkRbacPermission(Kanton, "create"):
raise PermissionError(f"User {self.userId} cannot create cantons")
# Ensure mandateId and featureInstanceId are set for proper data isolation
if not kanton.mandateId:
kanton.mandateId = self.mandateId
if not kanton.featureInstanceId:
kanton.featureInstanceId = self.featureInstanceId
self.db.recordCreate(Kanton, kanton.model_dump())
return kanton
def getKanton(self, kantonId: str) -> Optional[Kanton]:
"""Get a canton by ID."""
records = getRecordsetWithRBAC(
self.db,
Kanton,
self.currentUser,
recordFilter={"id": kantonId}
)
if not records:
return None
return Kanton(**records[0])
def getKantone(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Kanton]:
"""Get all cantons matching the filter."""
records = getRecordsetWithRBAC(
self.db,
Kanton,
self.currentUser,
recordFilter=recordFilter or {}
)
return [Kanton(**r) for r in records]
def updateKanton(self, kantonId: str, updateData: Dict[str, Any]) -> Optional[Kanton]:
"""Update a canton."""
kanton = self.getKanton(kantonId)
if not kanton:
return None
if not self.checkRbacPermission(Kanton, "update", kantonId):
raise PermissionError(f"User {self.userId} cannot modify canton {kantonId}")
for key, value in updateData.items():
if hasattr(kanton, key):
setattr(kanton, key, value)
self.db.recordModify(Kanton, kantonId, kanton.model_dump())
return kanton
def deleteKanton(self, kantonId: str) -> bool:
"""Delete a canton."""
kanton = self.getKanton(kantonId)
if not kanton:
return False
if not self.checkRbacPermission(Kanton, "delete", kantonId):
raise PermissionError(f"User {self.userId} cannot delete canton {kantonId}")
return self.db.recordDelete(Kanton, kantonId)
# ===== Land Methods =====
def createLand(self, land: Land) -> Land:
"""Create a new country."""
# Check RBAC permission
if not self.checkRbacPermission(Land, "create"):
raise PermissionError(f"User {self.userId} cannot create countries")
# Ensure mandateId and featureInstanceId are set for proper data isolation
if not land.mandateId:
land.mandateId = self.mandateId
if not land.featureInstanceId:
land.featureInstanceId = self.featureInstanceId
self.db.recordCreate(Land, land.model_dump())
return land
def getLand(self, landId: str) -> Optional[Land]:
"""Get a country by ID."""
records = getRecordsetWithRBAC(
self.db,
Land,
self.currentUser,
recordFilter={"id": landId}
)
if not records:
return None
return Land(**records[0])
def getLaender(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Land]:
"""Get all countries matching the filter."""
records = getRecordsetWithRBAC(
self.db,
Land,
self.currentUser,
recordFilter=recordFilter or {}
)
return [Land(**r) for r in records]
def updateLand(self, landId: str, updateData: Dict[str, Any]) -> Optional[Land]:
"""Update a country."""
land = self.getLand(landId)
if not land:
return None
if not self.checkRbacPermission(Land, "update", landId):
raise PermissionError(f"User {self.userId} cannot modify country {landId}")
for key, value in updateData.items():
if hasattr(land, key):
setattr(land, key, value)
self.db.recordModify(Land, landId, land.model_dump())
return land
def deleteLand(self, landId: str) -> bool:
"""Delete a country."""
land = self.getLand(landId)
if not land:
return False
if not self.checkRbacPermission(Land, "delete", landId):
raise PermissionError(f"User {self.userId} cannot delete country {landId}")
return self.db.recordDelete(Land, landId)
# ===== RBAC Permission Checks =====
def checkRbacPermission(
self,
modelClass: type,
operation: str,
recordId: Optional[str] = None
) -> bool:
"""
Check RBAC permission for a specific operation on a table.
Args:
modelClass: Pydantic model class for the table
operation: Operation to check ('create', 'update', 'delete', 'read')
recordId: Optional record ID for specific record check
Returns:
Boolean indicating permission
"""
if not self.rbac or not self.currentUser:
return False
tableName = modelClass.__name__
permissions = self.rbac.getUserPermissions(
self.currentUser,
AccessRuleContext.DATA,
tableName
)
if operation == "create":
return permissions.create != AccessLevel.NONE
elif operation == "update":
return permissions.update != AccessLevel.NONE
elif operation == "delete":
return permissions.delete != AccessLevel.NONE
elif operation == "read":
return permissions.read != AccessLevel.NONE
else:
return False
# ===== Direct Query Execution (stateless) =====
def executeQuery(self, queryText: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Execute a SQL query directly on the database (stateless).
WARNING: This method executes raw SQL. Ensure proper validation and sanitization
before calling this method. Consider implementing query whitelisting or
only allowing SELECT statements for production use.
Args:
queryText: SQL query string (preferably SELECT only)
parameters: Optional parameters for parameterized queries
Returns:
Dictionary with 'rows' (list of dicts), 'columns' (list of column names),
'rowCount' (int), and 'executionTime' (float)
"""
import time
try:
start_time = time.time()
# Ensure connection is alive
self.db._ensure_connection()
with self.db.connection.cursor() as cursor:
# Execute query
if parameters:
# Use parameterized query for safety
cursor.execute(queryText, parameters)
else:
cursor.execute(queryText)
# Fetch results
rows = cursor.fetchall()
# Convert to list of dictionaries
result_rows = []
if rows:
columns = [desc[0] for desc in cursor.description] if cursor.description else []
result_rows = [dict(zip(columns, row)) for row in rows]
else:
columns = []
execution_time = time.time() - start_time
return {
"rows": result_rows,
"columns": columns,
"rowCount": len(result_rows),
"executionTime": execution_time,
}
except Exception as e:
logger.error(f"Error executing query: {e}", exc_info=True)
raise
def getInterface(currentUser: User, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None) -> RealEstateObjects:
"""
Factory function to get or create a Real Estate interface instance for a user.
Uses singleton pattern per user.
Args:
currentUser: The authenticated user
mandateId: The mandate ID from RequestContext (X-Mandate-Id header). Required.
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header).
"""
effectiveMandateId = str(mandateId) if mandateId else None
effectiveFeatureInstanceId = str(featureInstanceId) if featureInstanceId else None
# Include featureInstanceId in key for proper isolation
userKey = f"{currentUser.id}_{effectiveMandateId}_{effectiveFeatureInstanceId}"
if userKey not in _realEstateInterfaces:
_realEstateInterfaces[userKey] = RealEstateObjects(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
else:
# Update user context if needed
_realEstateInterfaces[userKey].setUserContext(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
return _realEstateInterfaces[userKey]