import cleanup
This commit is contained in:
parent
a1d9c68604
commit
63e30c1281
55 changed files with 1691 additions and 149 deletions
10
app.py
10
app.py
|
|
@ -380,7 +380,6 @@ async def lifespan(app: FastAPI):
|
|||
# Register all feature definitions in RBAC catalog (for /api/features/ endpoint)
|
||||
try:
|
||||
from modules.security.rbacCatalog import getCatalogService
|
||||
from modules.system.registry import registerAllFeaturesInCatalog, syncCatalogFeaturesToDb
|
||||
catalogService = getCatalogService()
|
||||
registerAllFeaturesInCatalog(catalogService)
|
||||
logger.info("Feature catalog registration completed")
|
||||
|
|
@ -494,7 +493,6 @@ async def lifespan(app: FastAPI):
|
|||
def _onRunFailed(workflowId, runId, error, mandateId=None, workflowLabel=None):
|
||||
from modules.serviceCenter import getService
|
||||
from modules.serviceCenter.context import ServiceCenterContext
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelMessaging import MessagingEventParameters
|
||||
|
||||
rootInterface = getRootInterface()
|
||||
|
|
@ -555,6 +553,10 @@ async def lifespan(app: FastAPI):
|
|||
from modules.serviceCenter.services.serviceSubscription.enterpriseRenewalScheduler import registerEnterpriseRenewalScheduler
|
||||
registerEnterpriseRenewalScheduler()
|
||||
|
||||
# Register token and trusted device cleanup scheduler
|
||||
from modules.auth.trustedDeviceService import registerTokenCleanupScheduler
|
||||
registerTokenCleanupScheduler()
|
||||
|
||||
# Recover background jobs that were RUNNING when the previous worker died
|
||||
try:
|
||||
from modules.serviceCenter.services.serviceBackgroundJobs.mainBackgroundJobService import (
|
||||
|
|
@ -901,6 +903,10 @@ app.include_router(demoConfigRouter)
|
|||
from modules.routes.routeAdminDatabaseHealth import router as adminDatabaseHealthRouter
|
||||
app.include_router(adminDatabaseHealthRouter)
|
||||
|
||||
from modules.routes.routeAdminSessions import router as adminSessionsRouter, trustedDeviceRouter as adminTrustedDeviceRouter
|
||||
app.include_router(adminSessionsRouter)
|
||||
app.include_router(adminTrustedDeviceRouter)
|
||||
|
||||
from modules.routes.routeGdpr import router as gdprRouter
|
||||
app.include_router(gdprRouter)
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ Service_CLICKUP_OAUTH_REDIRECT_URI = http://localhost:8000/api/clickup/auth/conn
|
|||
|
||||
# Stripe Billing (both end with _SECRET for encryption script)
|
||||
STRIPE_SECRET_KEY_SECRET = DEV_ENC:Z0FBQUFBQnB5dkd5aHNGejgzQmpTdmprdzQxR19KZkh3MlhYUTNseFN3WnlaWjh2SDZyalN6aU9xSktkbUQwUnZrVnlvbGVRQm4yZFdiRU5aSEk5WVJuUnR4VUwtTm9OVk1WWmJQeU5QaDdib0hfVWV5U1BfYTFXRmdoOWdnOWxkb3JFQmF3bm45UjFUVUxmWGtGRkFKUGd6bmhpQlFnaVI3Q2lLdDlsY1VESk1vOEM0ZFBJNW1qcVZ0N2tPYmRLNmVKajZ2M3o3S05lWnRRVG5LdkRseW4wQ3VjNHNQZTZUdz09
|
||||
STRIPE_WEBHOOK_SECRET = DEV_ENC:Z0FBQUFBQnB5dkd5dDJMSHBrVk8wTzJhU2xzTTZCZWdvWmU2NGI2WklfRXRJZVUzaVYyOU9GLUZsalUwa2lPdEgtUHo0dVVvRDU1cy1saHJyU0Rxa2xQZjBuakExQzk3bmxBcU9WbEIxUEtpR1JoUFMxZG9ISGRZUXFhdFpSMGxvQUV3a0VLQllfUUtCOHZwTGdteV9rYTFOazBfSlN3ekNWblFpakJlZVlCTmNkWWQ4Sm01a1RCWTlnTlFHWVA0MkZYMlprUExrWFN2V0NVU1BTd1NKczFJbVo3VHpLdlc4UT09
|
||||
STRIPE_WEBHOOK_SECRET = DEV_ENC:Z0FBQUFBQnFLeUFlb2dfSjZPaWIyRjZsNjhiSDFQNFpxdW50YmlLUjFLX1lJMGdCWUtBUEdrRGhvSzVVWnkxNVZEdmtkQmk5X05YS0JVU1NyX3VQZTV2VjVwakd0RGM2WUl6TTlzbms1d1NCOTQtdURiVjhxdXZGVlR1ZVNTbUkwOFh1R04yUUxxay0=
|
||||
STRIPE_API_VERSION = 2026-01-28.clover
|
||||
STRIPE_AUTOMATIC_TAX_ENABLED = false
|
||||
STRIPE_TAX_RATE_ID_CH_VAT = txr_1TOQd14OUoIL0Osj7A0ZQlr0
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ Service_CLICKUP_OAUTH_REDIRECT_URI = https://api-int.poweron.swiss/api/clickup/a
|
|||
|
||||
# Stripe Billing (both end with _SECRET for encryption script)
|
||||
STRIPE_SECRET_KEY_SECRET = INT_ENC:Z0FBQUFBQnB5dkd5ekdBaGNGVUlOQUpncTlzLWlTV0V5OWZzQkpDczhCUGw4U1JpTHZ0d3pfYlFNWElLRlNiNlNsaDRYTGZUTkg2OUFrTW1GZXpOUjBVbmRQWjN6ekhHd2ZSQ195OHlaeWh1TmxrUm10V2R3YmdncmFLbFMzVjdqcWJMSUJPR2xuSEozclNoZG1rZVBTaWg3OFQ1Qzdxb0wyQ2RKazc2dG1aZXBUTXlvbDZqLS1KOVI5M3BGc3NQZkZRbnFpRjIwWmh2ZHlVNlpxZVo2dWNmMjQ5eW02QmtzUT09
|
||||
STRIPE_WEBHOOK_SECRET = INT_ENC:Z0FBQUFBQnA4UXZiUUVqTl9lREVRWTh1aHFDcFpwcXRkOUx4MS1ham9Ddkl6T0xzMnJuM1hhUHdGNG5CenY1MUg4RlJBOGFQTWl5cVd5MjJ2REItcHYyRmdLX3ZlT2p5Z3BRVkMtQnRoTVkteXlfaU92MVBtOEI0Ni1kbGlfa0NiRmFRRXNHLVE2NHI=
|
||||
STRIPE_WEBHOOK_SECRET = INT_ENC:Z0FBQUFBQnFLeUFWUUtMZ25NQ2ZOWE5nRF9CaFNwcXhSU2tKRktLaElLRHJMM295OXNkVEFLekVUMzN0YUpIZHJfWGNqa0xxOFZRVHZEUXVLZ3ItVGZWc2VFQ2thcUlJalY1b0JDSmR6RF96d1A3OGhyd0w1MHZPeFNZRkl0c19kYUJQcHVwR2tsd0s=
|
||||
STRIPE_API_VERSION = 2026-01-28.clover
|
||||
STRIPE_AUTOMATIC_TAX_ENABLED = false
|
||||
STRIPE_TAX_RATE_ID_CH_VAT = txr_1TOQd14OUoIL0Osj7A0ZQlr0
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ Service_CLICKUP_OAUTH_REDIRECT_URI = https://api.poweron.swiss/api/clickup/auth/
|
|||
|
||||
# Stripe Billing (both end with _SECRET for encryption script)
|
||||
STRIPE_SECRET_KEY_SECRET = PROD_ENC:Z0FBQUFBQnB5dkd6aVA3R3VRS3VHMUgzUEVjYkR4eUZKWFhPUzFTTVlHNnBvT3FienNQaUlBWVpPLXJyVGpGMWk4LXktMXphX0J6ZTVESkJxdjNNa3ZJbF9wX2ppYzdjYlF0cmdVamlEWWJDSmJYYkJseHctTlh4dnNoQWs4SG5haVl2TTNDdXpuaFpqeDBtNkFCbUxMa0RaWG14dmxyOEdILTNrZ2licmNpbXVkN2lFSWoxZW1BODNpV0ZTQ0VaeXRmR1d4RjExMlVFS3MtQU9zZXZlZE1mTmY3OWctUXJHdz09
|
||||
STRIPE_WEBHOOK_SECRET = PROD_ENC:Z0FBQUFBQnBudkpGNUpTWldsakYydFhFelBrR1lSaWxYT3kyMENOMUljZTJUZHBWcEhhdWVCMzYxZXQ5b3VlTFVRalFiTVdsbGxrdUx0RDFwSEpsOC1sTDJRTEJNQlA3S3ZaQzBtV1h6bWp5VnlMZUgwUlF3cXYxcnljZVE5SWdzLVg3V0syOWRYS08=
|
||||
STRIPE_WEBHOOK_SECRET = PROD_ENC:Z0FBQUFBQnFLeUFNQ1FhVE94ZzM3V3NCVGVVWnltUndsOG1Ra0hQTmJ3QWY5aXVWeTJsX3A4a3VBSnFQd3drWFRZNFVDdWxCeFgyQ0RpNGQ0SlJOcm9tVE5KZmVqQU1WUjFjeDRJeGE5THdmR0g1V2dQUk5SSjcySnAzR245NW5NUFVDT3lJUWpjWFo=
|
||||
STRIPE_API_VERSION = 2026-01-28.clover
|
||||
STRIPE_AUTOMATIC_TAX_ENABLED = false
|
||||
STRIPE_TAX_RATE_ID_CH_VAT = txr_1TOQZG8WqlVsabrfFEu49pah
|
||||
|
|
|
|||
|
|
@ -169,3 +169,51 @@ def _getClientIp(request: Request) -> Optional[str]:
|
|||
if request.client:
|
||||
return request.client.host
|
||||
return None
|
||||
|
||||
|
||||
# --- Scheduler Integration ---
|
||||
|
||||
async def _runTokenAndDeviceCleanup() -> None:
|
||||
"""Scheduled task: remove expired tokens and trusted devices."""
|
||||
try:
|
||||
from modules.connectors.connectorDbPostgre import ConnectorPostgre
|
||||
|
||||
db = ConnectorPostgre("poweron_app")
|
||||
now = getUtcTimestamp()
|
||||
|
||||
# Expired auth-session tokens
|
||||
tokens = db.getRecordset(
|
||||
Token,
|
||||
recordFilter={"tokenPurpose": TokenPurpose.AUTH_SESSION.value},
|
||||
)
|
||||
expiredCount = 0
|
||||
for t in tokens:
|
||||
if t.get("expiresAt", 0) < now:
|
||||
db.recordDelete(Token, t["id"])
|
||||
expiredCount += 1
|
||||
|
||||
# Expired trusted devices
|
||||
deviceCount = cleanupExpiredDevices(db)
|
||||
|
||||
if expiredCount or deviceCount:
|
||||
logger.info(
|
||||
f"Token cleanup: {expiredCount} expired token(s), "
|
||||
f"{deviceCount} expired trusted device(s) removed"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Token/device cleanup failed: {e}")
|
||||
|
||||
|
||||
def registerTokenCleanupScheduler() -> None:
|
||||
"""Register daily token cleanup job. Call during app startup."""
|
||||
try:
|
||||
from modules.shared.eventManagement import eventManager
|
||||
|
||||
eventManager.registerCron(
|
||||
jobId="token_device_cleanup",
|
||||
func=_runTokenAndDeviceCleanup,
|
||||
cronKwargs={"hour": "4", "minute": "0"},
|
||||
)
|
||||
logger.info("Token/device cleanup scheduler registered (daily 04:00)")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to register token cleanup scheduler: {e}")
|
||||
|
|
|
|||
|
|
@ -261,35 +261,29 @@ class CommcoachObjects:
|
|||
# =========================================================================
|
||||
|
||||
def getPersonas(self, userId: str, instanceId: str) -> List[Dict[str, Any]]:
|
||||
from .datamodelCommcoach import CoachingPersona
|
||||
builtins = self.db.getRecordset(CoachingPersona, recordFilter={"userId": "system"})
|
||||
custom = self.db.getRecordset(CoachingPersona, recordFilter={"userId": userId, "instanceId": instanceId})
|
||||
all = builtins + custom
|
||||
return [p for p in all if p.get("isActive", True)]
|
||||
|
||||
def getPersona(self, personaId: str) -> Optional[Dict[str, Any]]:
|
||||
from .datamodelCommcoach import CoachingPersona
|
||||
records = self.db.getRecordset(CoachingPersona, recordFilter={"id": personaId})
|
||||
return records[0] if records else None
|
||||
|
||||
def createPersona(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
from .datamodelCommcoach import CoachingPersona
|
||||
data["createdAt"] = getIsoTimestamp()
|
||||
data["updatedAt"] = getIsoTimestamp()
|
||||
return self.db.recordCreate(CoachingPersona, data)
|
||||
|
||||
def updatePersona(self, personaId: str, updates: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
from .datamodelCommcoach import CoachingPersona
|
||||
updates["updatedAt"] = getIsoTimestamp()
|
||||
return self.db.recordModify(CoachingPersona, personaId, updates)
|
||||
|
||||
def deletePersona(self, personaId: str) -> bool:
|
||||
from .datamodelCommcoach import CoachingPersona
|
||||
return self.db.recordDelete(CoachingPersona, personaId)
|
||||
|
||||
def getAllPersonas(self, instanceId: str) -> List[Dict[str, Any]]:
|
||||
"""All personas (builtin + custom for this instance), including inactive."""
|
||||
from .datamodelCommcoach import CoachingPersona
|
||||
builtins = self.db.getRecordset(CoachingPersona, recordFilter={"userId": "system"})
|
||||
custom = self.db.getRecordset(CoachingPersona, recordFilter={"instanceId": instanceId})
|
||||
custom = [p for p in custom if p.get("userId") != "system"]
|
||||
|
|
@ -300,11 +294,9 @@ class CommcoachObjects:
|
|||
# =========================================================================
|
||||
|
||||
def getModulePersonas(self, moduleId: str) -> List[Dict[str, Any]]:
|
||||
from .datamodelCommcoach import ModulePersonaMapping
|
||||
return self.db.getRecordset(ModulePersonaMapping, recordFilter={"moduleId": moduleId})
|
||||
|
||||
def setModulePersonas(self, moduleId: str, personaIds: List[str], instanceId: str) -> List[Dict[str, Any]]:
|
||||
from .datamodelCommcoach import ModulePersonaMapping
|
||||
existing = self.db.getRecordset(ModulePersonaMapping, recordFilter={"moduleId": moduleId})
|
||||
for rec in existing:
|
||||
self.db.recordDelete(ModulePersonaMapping, rec["id"])
|
||||
|
|
@ -325,18 +317,15 @@ class CommcoachObjects:
|
|||
# =========================================================================
|
||||
|
||||
def getBadges(self, userId: str, instanceId: str) -> List[Dict[str, Any]]:
|
||||
from .datamodelCommcoach import CoachingBadge
|
||||
records = self.db.getRecordset(CoachingBadge, recordFilter={"userId": userId, "instanceId": instanceId})
|
||||
records.sort(key=lambda r: r.get("awardedAt") or 0, reverse=True)
|
||||
return records
|
||||
|
||||
def hasBadge(self, userId: str, instanceId: str, badgeKey: str) -> bool:
|
||||
from .datamodelCommcoach import CoachingBadge
|
||||
records = self.db.getRecordset(CoachingBadge, recordFilter={"userId": userId, "instanceId": instanceId, "badgeKey": badgeKey})
|
||||
return len(records) > 0
|
||||
|
||||
def awardBadge(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
from .datamodelCommcoach import CoachingBadge
|
||||
data["awardedAt"] = getUtcTimestamp()
|
||||
data["createdAt"] = getIsoTimestamp()
|
||||
return self.db.recordCreate(CoachingBadge, data)
|
||||
|
|
|
|||
|
|
@ -333,7 +333,6 @@ async def startSession(
|
|||
try:
|
||||
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
|
||||
voiceInterface = getVoiceInterface(context.user, mandateId)
|
||||
from .serviceCommcoach import getUserVoicePrefs, stripMarkdownForTts, buildTtsConfigErrorMessage
|
||||
language, voiceName = getUserVoicePrefs(userId, mandateId)
|
||||
ttsResult = await voiceInterface.textToSpeech(
|
||||
text=stripMarkdownForTts(greetingText),
|
||||
|
|
@ -378,7 +377,6 @@ async def startSession(
|
|||
asyncio.create_task(service.processSessionOpening(sessionId, moduleId, interface))
|
||||
|
||||
async def _newSessionEventGenerator():
|
||||
from modules.shared.timeUtils import getIsoTimestamp
|
||||
timeoutCount = 0
|
||||
try:
|
||||
while True:
|
||||
|
|
@ -468,7 +466,6 @@ async def cancelSession(
|
|||
raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
|
||||
_validateOwnership(session, context)
|
||||
|
||||
from modules.shared.timeUtils import getUtcTimestamp
|
||||
interface.updateSession(sessionId, {
|
||||
"status": CoachingSessionStatus.CANCELLED.value,
|
||||
"endedAt": getUtcTimestamp(),
|
||||
|
|
@ -581,7 +578,6 @@ async def sendAudioStream(
|
|||
if not audioBody:
|
||||
raise HTTPException(status_code=400, detail=routeApiMsg("No audio data received"))
|
||||
|
||||
from .serviceCommcoach import getUserVoicePrefs
|
||||
language, _ = getUserVoicePrefs(str(context.user.id), mandateId)
|
||||
|
||||
moduleId = session.get("moduleId")
|
||||
|
|
@ -765,7 +761,6 @@ async def updateTaskStatus(
|
|||
|
||||
updates = {"status": body.status.value}
|
||||
if body.status == CoachingTaskStatus.DONE:
|
||||
from modules.shared.timeUtils import getUtcTimestamp
|
||||
updates["completedAt"] = getUtcTimestamp()
|
||||
|
||||
updated = interface.updateTask(taskId, updates)
|
||||
|
|
|
|||
|
|
@ -98,7 +98,6 @@ def getUserVoicePrefs(userId: str, mandateId: Optional[str] = None) -> tuple:
|
|||
"""Load voice language and voiceName from central UserVoicePreferences.
|
||||
Returns (language, voiceName) tuple."""
|
||||
try:
|
||||
from modules.datamodels.datamodelUam import UserVoicePreferences
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
rootIf = getRootInterface()
|
||||
prefs = rootIf.db.getRecordset(
|
||||
|
|
@ -430,7 +429,6 @@ async def _resolveDocumentIntent(combinedUserPrompt: str, docs: List[Dict[str, A
|
|||
"""Pre-AI-call: identify which documents the user references and what action is needed."""
|
||||
if not docs:
|
||||
return {"read": [], "update": [], "create": [], "noDocumentAction": True}
|
||||
from . import serviceCommcoachAi as aiPrompts
|
||||
docCatalog = [{"id": d.get("id", ""), "title": d.get("summary") or d.get("fileName", ""), "summary": (d.get("summary") or "")[:100]} for d in docs]
|
||||
prompt = aiPrompts.buildDocumentIntentPrompt(combinedUserPrompt, docCatalog)
|
||||
try:
|
||||
|
|
@ -744,7 +742,6 @@ class CommcoachService:
|
|||
4. Map agent events to CommCoach SSE events
|
||||
5. Post-processing: store message, TTS, tasks, scores
|
||||
"""
|
||||
from . import interfaceFeatureCommcoach as interfaceDb
|
||||
|
||||
# Store user message
|
||||
userMsg = CoachingMessage(
|
||||
|
|
@ -907,7 +904,6 @@ class CommcoachService:
|
|||
)
|
||||
agentService = getService("agent", serviceContext)
|
||||
|
||||
from modules.datamodels.datamodelAi import PriorityEnum, OperationTypeEnum
|
||||
config = AgentConfig(
|
||||
toolSet="commcoach" if useTools else "none",
|
||||
maxRounds=3 if useTools else 1,
|
||||
|
|
|
|||
|
|
@ -157,7 +157,6 @@ class ListProcessor:
|
|||
processedAttrs[attrName] = self.string_parser.mapping[attrValue]
|
||||
else:
|
||||
# Check if attribute value matches any data patterns
|
||||
from .subPatterns import findPatternsInText, DataPatterns
|
||||
matches = findPatternsInText(attrValue, DataPatterns.patterns)
|
||||
if matches:
|
||||
patternName = matches[0][0]
|
||||
|
|
@ -191,7 +190,6 @@ class ListProcessor:
|
|||
# Skip if already a placeholder
|
||||
if not self.string_parser._isPlaceholder(text):
|
||||
# Check if text matches any patterns
|
||||
from .subPatterns import findPatternsInText, DataPatterns
|
||||
patternMatches = findPatternsInText(text, DataPatterns.patterns)
|
||||
|
||||
if patternMatches:
|
||||
|
|
|
|||
|
|
@ -796,7 +796,6 @@ class RealEstateObjects:
|
|||
return False
|
||||
|
||||
tableName = modelClass.__name__
|
||||
from modules.interfaces.interfaceRbac import buildDataObjectKey
|
||||
objectKey = buildDataObjectKey(tableName, featureCode=self.featureCode if hasattr(self, 'featureCode') else None)
|
||||
permissions = self.rbac.getUserPermissions(
|
||||
self.currentUser,
|
||||
|
|
|
|||
|
|
@ -328,7 +328,6 @@ async def startSession(
|
|||
if context.isSysAdmin and joinMode == TeamsbotJoinMode.SYSTEM_BOT:
|
||||
systemBot = interface.getActiveSystemBot(mandateId)
|
||||
if not systemBot:
|
||||
from .datamodelTeamsbot import TeamsbotSystemBot
|
||||
allBots = interface.db.getRecordset(TeamsbotSystemBot, recordFilter={"isActive": True})
|
||||
if allBots:
|
||||
systemBot = allBots[0]
|
||||
|
|
@ -537,7 +536,6 @@ async def streamSession(
|
|||
|
||||
async def _eventGenerator():
|
||||
"""Generate SSE events from the session event queue."""
|
||||
from .service import sessionEvents
|
||||
|
||||
# Send initial session state with stats
|
||||
stats = interface.getSessionStats(sessionId)
|
||||
|
|
@ -545,7 +543,6 @@ async def streamSession(
|
|||
|
||||
# Send current bot WebSocket connection state so the operator UI can
|
||||
# render the live indicator without waiting for the next connect/disconnect.
|
||||
from .service import getActiveService as _getActiveService
|
||||
yield f"data: {json.dumps({'type': 'botConnectionState', 'data': {'connected': _getActiveService(sessionId) is not None}})}\n\n"
|
||||
|
||||
# Stream events
|
||||
|
|
@ -1040,7 +1037,6 @@ async def submitDirectorPrompt(
|
|||
detail=routeApiMsg(f"Too many files ({len(fileIds)}); max {DIRECTOR_PROMPT_FILE_LIMIT}"),
|
||||
)
|
||||
|
||||
from .service import getActiveService
|
||||
service = getActiveService(sessionId)
|
||||
if not service:
|
||||
raise HTTPException(
|
||||
|
|
@ -1108,7 +1104,6 @@ async def deleteDirectorPrompt(
|
|||
if not context.isPlatformAdmin and prompt.get("operatorUserId") != str(context.user.id):
|
||||
raise HTTPException(status_code=404, detail=f"Prompt '{promptId}' not found")
|
||||
|
||||
from .service import getActiveService
|
||||
service = getActiveService(sessionId)
|
||||
if service:
|
||||
await service.removePersistentPrompt(promptId)
|
||||
|
|
@ -1134,7 +1129,6 @@ async def testVoice(
|
|||
):
|
||||
"""Test TTS voice with AI-generated sample text in the correct language."""
|
||||
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
|
||||
from .service import createAiService
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum
|
||||
|
||||
mandateId = _validateInstanceAccess(instanceId, context)
|
||||
|
|
@ -1547,7 +1541,6 @@ async def postTranscript(
|
|||
originalUser = rootUser
|
||||
|
||||
# Process transcript through the service pipeline
|
||||
from .service import TeamsbotService
|
||||
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
|
||||
|
||||
service = TeamsbotService(originalUser, mandateId, instanceId, config)
|
||||
|
|
@ -1600,7 +1593,6 @@ async def postBotStatus(
|
|||
if not originalUser:
|
||||
originalUser = rootUser
|
||||
|
||||
from .service import TeamsbotService
|
||||
service = TeamsbotService(originalUser, mandateId, instanceId, config)
|
||||
|
||||
interface = interfaceDb.getInterface(originalUser, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
|
|
@ -1640,7 +1632,6 @@ async def botWebsocket(
|
|||
|
||||
# Load the original user who started the session (has RBAC roles in mandate)
|
||||
# Bot callbacks have no HTTP auth, so we reconstruct the user context from the session record.
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
|
||||
rootInterface = getRootInterface()
|
||||
rootUser = rootInterface.currentUser
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ class AccountingBridge:
|
|||
|
||||
async def getActiveConfig(self, featureInstanceId: str) -> Optional[Dict[str, Any]]:
|
||||
"""Load the active TrusteeAccountingConfig for a feature instance."""
|
||||
from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig
|
||||
records = self._trusteeInterface.db.getRecordset(
|
||||
TrusteeAccountingConfig,
|
||||
recordFilter={"featureInstanceId": featureInstanceId, "isActive": True},
|
||||
|
|
@ -128,7 +127,6 @@ class AccountingBridge:
|
|||
Optional _resolved* params allow pushBatchToAccounting to pass a pre-resolved
|
||||
connector/config so we don't decrypt per position (avoids rate-limit).
|
||||
"""
|
||||
from modules.features.trustee.datamodelFeatureTrustee import TrusteePosition, TrusteeAccountingSync
|
||||
|
||||
connector = _resolvedConnector
|
||||
plainConfig = _resolvedPlainConfig
|
||||
|
|
@ -306,7 +304,6 @@ class AccountingBridge:
|
|||
|
||||
# Update last sync on config record
|
||||
if configRecord:
|
||||
from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig
|
||||
updatePayload = {
|
||||
"lastSyncAt": time.time(),
|
||||
"lastSyncStatus": "success" if result.success else "error",
|
||||
|
|
@ -335,7 +332,6 @@ class AccountingBridge:
|
|||
|
||||
async def refreshChartOfAccounts(self, featureInstanceId: str) -> List[AccountingChart]:
|
||||
"""Fetch the full chart of accounts from the external system and cache it locally on TrusteeAccountingConfig."""
|
||||
from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig
|
||||
|
||||
connector, plainConfig, configRecord = await self._resolveConnectorAndConfig(featureInstanceId)
|
||||
if not connector or not plainConfig or not configRecord:
|
||||
|
|
|
|||
|
|
@ -309,7 +309,6 @@ class TrusteeObjects:
|
|||
return False
|
||||
|
||||
tableName = modelClass.__name__
|
||||
from modules.interfaces.interfaceRbac import buildDataObjectKey
|
||||
objectKey = buildDataObjectKey(tableName, featureCode=self.featureCode if hasattr(self, 'featureCode') else None)
|
||||
permissions = self.rbac.getUserPermissions(
|
||||
self.currentUser,
|
||||
|
|
@ -338,7 +337,6 @@ class TrusteeObjects:
|
|||
return AccessLevel.NONE
|
||||
|
||||
tableName = modelClass.__name__
|
||||
from modules.interfaces.interfaceRbac import buildDataObjectKey
|
||||
objectKey = buildDataObjectKey(tableName, featureCode=self.featureCode if hasattr(self, 'featureCode') else None)
|
||||
permissions = self.rbac.getUserPermissions(
|
||||
self.currentUser,
|
||||
|
|
|
|||
|
|
@ -170,7 +170,6 @@ def getQuickActions(
|
|||
if role and role.roleLabel:
|
||||
userRoleLabels.add(role.roleLabel)
|
||||
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
|
||||
lang = (language or "de").strip() or "de"
|
||||
|
||||
|
|
@ -1201,7 +1200,6 @@ def _buildSyncStatusByPosition(interface, instanceId: str) -> Dict[str, Dict[str
|
|||
``error``, so a successful retry hides an old failure. Any other status
|
||||
(`pending`, `cancelled`, ...) is kept verbatim.
|
||||
"""
|
||||
from .datamodelFeatureTrustee import TrusteeAccountingSync
|
||||
|
||||
syncRecords = interface.db.getRecordset(
|
||||
TrusteeAccountingSync, recordFilter={"featureInstanceId": instanceId}
|
||||
|
|
@ -1290,7 +1288,6 @@ def _handlePositionMode(instanceId, mandateId, mode, column, pagination, context
|
|||
"""Handle mode=filterValues and mode=ids for trustee positions."""
|
||||
from modules.dbHelpers.paginationHelpers import handleIdsInMemory, handleFilterValuesInMemory
|
||||
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
|
||||
from .datamodelFeatureTrustee import TrusteePositionView
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
if mode == "filterValues":
|
||||
if not column:
|
||||
|
|
@ -1507,7 +1504,6 @@ def delete_accounting_config(
|
|||
"""Remove the accounting integration for this instance."""
|
||||
mandateId = _validateInstanceAccess(instanceId, context)
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
from .datamodelFeatureTrustee import TrusteeAccountingConfig
|
||||
records = interface.db.getRecordset(TrusteeAccountingConfig, recordFilter={"featureInstanceId": instanceId})
|
||||
for r in records:
|
||||
interface.db.recordDelete(TrusteeAccountingConfig, r.get("id"))
|
||||
|
|
@ -1602,7 +1598,6 @@ def get_sync_status(
|
|||
"""Get sync status of all positions for this instance."""
|
||||
mandateId = _validateInstanceAccess(instanceId, context)
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
from .datamodelFeatureTrustee import TrusteeAccountingSync
|
||||
records = interface.db.getRecordset(TrusteeAccountingSync, recordFilter={"featureInstanceId": instanceId})
|
||||
return {"items": [{k: v for k, v in r.items() if not k.startswith("_")} for r in records]}
|
||||
|
||||
|
|
@ -1618,7 +1613,6 @@ def get_position_sync_status(
|
|||
"""Get sync status for a specific position."""
|
||||
mandateId = _validateInstanceAccess(instanceId, context)
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
from .datamodelFeatureTrustee import TrusteeAccountingSync
|
||||
records = interface.db.getRecordset(TrusteeAccountingSync, recordFilter={"positionId": positionId, "featureInstanceId": instanceId})
|
||||
return {"items": [{k: v for k, v in r.items() if not k.startswith("_")} for r in records]}
|
||||
|
||||
|
|
@ -1776,7 +1770,6 @@ def _serializeRoleForApi(role) -> Dict[str, Any]:
|
|||
here (same pattern as ``getQuickActions``). Without this the React tree
|
||||
crashes with "Objects are not valid as a React child".
|
||||
"""
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
payload = role.model_dump()
|
||||
payload["description"] = resolveText(payload.get("description"))
|
||||
return payload
|
||||
|
|
|
|||
|
|
@ -269,7 +269,6 @@ async def _extractWithAi(
|
|||
) -> Dict[str, Any]:
|
||||
"""3-step extraction: (1a) OCR/text via Vision AI, (1b) classify text, (2) structure by type."""
|
||||
await self.services.ai.ensureAiObjectsInitialized()
|
||||
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentItemReference
|
||||
|
||||
docList = DocumentReferenceList(
|
||||
references=[DocumentItemReference(documentId=chatDocumentId, fileName=fileName)]
|
||||
|
|
|
|||
|
|
@ -917,7 +917,6 @@ async def _runWorkspaceAgent(
|
|||
messagePersisted = False
|
||||
_toolSet = _cfg.get("toolSet", "core")
|
||||
_agentCfg = _cfg.get("agentConfig")
|
||||
from modules.serviceCenter.services.serviceAgent.datamodelAgent import AgentConfig
|
||||
|
||||
agentCfgDict = dict(_agentCfg) if isinstance(_agentCfg, dict) else {}
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -465,7 +465,6 @@ class AiObjects:
|
|||
toolChoice: Any = None,
|
||||
) -> AsyncGenerator[Union[str, AiCallResponse], None]:
|
||||
"""Stream a model call. Yields str deltas, then final AiCallResponse with billing."""
|
||||
from modules.datamodels.datamodelAi import AiModelCall, AiModelResponse
|
||||
|
||||
inputBytes = sum(len(str(m.get("content", "")).encode("utf-8")) for m in messages)
|
||||
startTime = time.time()
|
||||
|
|
@ -537,7 +536,6 @@ class AiObjects:
|
|||
Returns:
|
||||
AiCallResponse with metadata["embeddings"] containing the vectors.
|
||||
"""
|
||||
from modules.aicore.aicoreBase import ContextLengthExceededException
|
||||
|
||||
if options is None:
|
||||
options = AiCallOptions(operationType=OperationTypeEnum.EMBEDDING)
|
||||
|
|
|
|||
|
|
@ -220,7 +220,6 @@ class AppObjects:
|
|||
|
||||
tableName = modelClass.__name__
|
||||
# Use buildDataObjectKey for semantic namespace lookup
|
||||
from modules.interfaces.interfaceRbac import buildDataObjectKey
|
||||
objectKey = buildDataObjectKey(tableName)
|
||||
permissions = self.rbac.getUserPermissions(
|
||||
self.currentUser,
|
||||
|
|
@ -1122,8 +1121,6 @@ class AppObjects:
|
|||
def _deleteUserReferencedData(self, userId: str) -> None:
|
||||
"""Deletes all data associated with a user (full cascade)."""
|
||||
try:
|
||||
from modules.datamodels.datamodelNotification import UserNotification
|
||||
from modules.datamodels.datamodelInvitation import Invitation
|
||||
|
||||
# 1. FeatureAccess + FeatureAccessRole
|
||||
accesses = self.db.getRecordset(FeatureAccess, recordFilter={"userId": userId})
|
||||
|
|
@ -1560,7 +1557,6 @@ class AppObjects:
|
|||
|
||||
# Copy system template roles to new mandate (admin, user, viewer + AccessRules)
|
||||
try:
|
||||
from modules.interfaces.interfaceRbac import copySystemRolesToMandate
|
||||
copiedCount = copySystemRolesToMandate(self.db, mandateId)
|
||||
logger.info(f"Copied {copiedCount} system roles to new mandate {mandateId}")
|
||||
except Exception as e:
|
||||
|
|
@ -1576,8 +1572,6 @@ class AppObjects:
|
|||
``mandateLabel`` is the display name (Voller Name); a unique slug ``name`` (Kurzzeichen) is derived.
|
||||
"""
|
||||
from modules.datamodels.datamodelSubscription import MandateSubscription, SubscriptionStatusEnum, BUILTIN_PLANS
|
||||
from modules.datamodels.datamodelFeatures import FeatureInstance
|
||||
from modules.interfaces.interfaceRbac import copySystemRolesToMandate
|
||||
from modules.interfaces.interfaceFeatures import getFeatureInterface
|
||||
from modules.shared.featureDiscovery import loadFeatureMainModules
|
||||
plan = BUILTIN_PLANS.get(planKey)
|
||||
|
|
@ -1847,7 +1841,6 @@ class AppObjects:
|
|||
raise PermissionError(f"No permission to delete mandate {mandateId}")
|
||||
|
||||
if not force:
|
||||
from modules.shared.timeUtils import getUtcTimestamp
|
||||
self.db.recordModify(Mandate, mandateId, {"enabled": False, "deletedAt": getUtcTimestamp()})
|
||||
logger.info(f"Soft-deleted mandate {mandateId} (30-day retention)")
|
||||
return True
|
||||
|
|
@ -1858,8 +1851,6 @@ class AppObjects:
|
|||
from modules.datamodels.datamodelFiles import FileItem
|
||||
from modules.datamodels.datamodelDataSource import DataSource
|
||||
from modules.datamodels.datamodelKnowledge import FileContentIndex, ContentChunk
|
||||
from modules.datamodels.datamodelFeatures import FeatureDataSource
|
||||
from modules.datamodels.datamodelFeatures import DataNeutralizerAttributes
|
||||
|
||||
instances = self.db.getRecordset(FeatureInstance, recordFilter={"mandateId": mandateId})
|
||||
|
||||
|
|
@ -1983,7 +1974,6 @@ class AppObjects:
|
|||
# 3b. Billing data cascade handled by onMandateDelete lifecycle hook (interfaceDbBilling)
|
||||
|
||||
# 3c. Delete Invitations for this mandate
|
||||
from modules.datamodels.datamodelInvitation import Invitation
|
||||
invitations = self.db.getRecordset(Invitation, recordFilter={"mandateId": mandateId})
|
||||
for inv in invitations:
|
||||
self.db.recordDelete(Invitation, inv.get("id"))
|
||||
|
|
@ -1991,7 +1981,6 @@ class AppObjects:
|
|||
logger.info(f"Cascade: deleted {len(invitations)} Invitations for mandate {mandateId}")
|
||||
|
||||
# 4. Delete mandate-level Roles
|
||||
from modules.datamodels.datamodelRbac import Role, AccessRule
|
||||
roles = self.db.getRecordset(Role, recordFilter={"mandateId": mandateId})
|
||||
for role in roles:
|
||||
rules = self.db.getRecordset(AccessRule, recordFilter={"roleId": role.get("id")})
|
||||
|
|
@ -3961,7 +3950,6 @@ class AppObjects:
|
|||
|
||||
def getTableListViews(self, contextKey: str) -> list:
|
||||
"""Return all saved views for the current user and contextKey."""
|
||||
from modules.datamodels.datamodelPagination import TableListView
|
||||
try:
|
||||
rows = self.db.getRecordset(
|
||||
TableListView,
|
||||
|
|
@ -3980,7 +3968,6 @@ class AppObjects:
|
|||
|
||||
def getTableListView(self, contextKey: str, viewKey: str):
|
||||
"""Return one view by viewKey or None if not found."""
|
||||
from modules.datamodels.datamodelPagination import TableListView
|
||||
try:
|
||||
rows = self.db.getRecordset(
|
||||
TableListView,
|
||||
|
|
@ -3996,8 +3983,6 @@ class AppObjects:
|
|||
|
||||
def createTableListView(self, contextKey: str, viewKey: str, displayName: str, config: dict):
|
||||
"""Create a new view. Raises ValueError if viewKey already exists for this context."""
|
||||
from modules.datamodels.datamodelPagination import TableListView
|
||||
from modules.shared.timeUtils import getUtcTimestamp
|
||||
if self.getTableListView(contextKey=contextKey, viewKey=viewKey) is not None:
|
||||
raise ValueError(f"View '{viewKey}' already exists for context '{contextKey}'")
|
||||
data = {
|
||||
|
|
@ -4018,8 +4003,6 @@ class AppObjects:
|
|||
|
||||
def updateTableListView(self, viewId: str, updates: dict):
|
||||
"""Update an existing view by its primary key id."""
|
||||
from modules.datamodels.datamodelPagination import TableListView
|
||||
from modules.shared.timeUtils import getUtcTimestamp
|
||||
try:
|
||||
updates = {**updates, "updatedAt": getUtcTimestamp()}
|
||||
self.db.recordModify(TableListView, viewId, updates)
|
||||
|
|
@ -4034,7 +4017,6 @@ class AppObjects:
|
|||
|
||||
def deleteTableListView(self, viewId: str) -> bool:
|
||||
"""Delete a view by primary key id. Returns True on success."""
|
||||
from modules.datamodels.datamodelPagination import TableListView
|
||||
try:
|
||||
self.db.recordDelete(TableListView, viewId)
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1654,8 +1654,6 @@ class BillingObjects:
|
|||
`amount` column. Resolves matching mandate/user IDs via the app DB
|
||||
first, then builds a single SQL query with OR-combined conditions.
|
||||
"""
|
||||
from modules.connectors.connectorDbPostgre import getModelFields, parseRecordFields
|
||||
from modules.datamodels.datamodelUam import UserInDB
|
||||
from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
|
||||
|
||||
table = BillingTransaction.__name__
|
||||
|
|
|
|||
|
|
@ -393,7 +393,6 @@ class ChatObjects:
|
|||
|
||||
tableName = modelClass.__name__
|
||||
# Use buildDataObjectKey for semantic namespace lookup
|
||||
from modules.interfaces.interfaceRbac import buildDataObjectKey
|
||||
objectKey = buildDataObjectKey(tableName)
|
||||
permissions = self.rbac.getUserPermissions(
|
||||
self.currentUser,
|
||||
|
|
@ -826,7 +825,6 @@ class ChatObjects:
|
|||
if not effectiveMandateId:
|
||||
# Fall back to Root mandate (first mandate in system)
|
||||
try:
|
||||
from modules.datamodels.datamodelUam import Mandate
|
||||
from modules.security.rootAccess import getRootDbAppConnector
|
||||
dbAppConn = getRootDbAppConnector()
|
||||
allMandates = dbAppConn.getRecordset(Mandate)
|
||||
|
|
|
|||
|
|
@ -741,7 +741,6 @@ def migrateVectorDimensions():
|
|||
If it differs from the target, nulls existing embeddings and alters the column type.
|
||||
Safe to call on every startup — skips when dimensions already match or table doesn't exist.
|
||||
"""
|
||||
from modules.datamodels.datamodelKnowledge import KNOWLEDGE_EMBEDDING_DIMENSIONS
|
||||
targetDim = KNOWLEDGE_EMBEDDING_DIMENSIONS
|
||||
|
||||
interface = getInterface()
|
||||
|
|
|
|||
|
|
@ -317,7 +317,6 @@ class ComponentObjects:
|
|||
return False
|
||||
|
||||
tableName = modelClass.__name__
|
||||
from modules.interfaces.interfaceRbac import buildDataObjectKey
|
||||
objectKey = buildDataObjectKey(tableName)
|
||||
permissions = self.rbac.getUserPermissions(
|
||||
self.currentUser,
|
||||
|
|
@ -1066,7 +1065,6 @@ class ComponentObjects:
|
|||
Owners always can. Non-owners need RBAC ALL level."""
|
||||
if self._isFolderOwner(folder):
|
||||
return
|
||||
from modules.interfaces.interfaceRbac import buildDataObjectKey
|
||||
objectKey = buildDataObjectKey("FileFolder")
|
||||
permissions = self.rbac.getUserPermissions(
|
||||
self.currentUser, AccessRuleContext.DATA, objectKey,
|
||||
|
|
@ -1207,7 +1205,6 @@ class ComponentObjects:
|
|||
self._requireFolderWriteAccess(folder, folderId, "update")
|
||||
|
||||
if scope == "global":
|
||||
from modules.interfaces.interfaceRbac import buildDataObjectKey
|
||||
objectKey = buildDataObjectKey("FileFolder")
|
||||
permissions = self.rbac.getUserPermissions(
|
||||
self.currentUser, AccessRuleContext.DATA, objectKey,
|
||||
|
|
@ -1387,8 +1384,6 @@ class ComponentObjects:
|
|||
Owners always can. Non-owners need RBAC ALL level."""
|
||||
if self._isFileOwner(file):
|
||||
return
|
||||
from modules.interfaces.interfaceRbac import buildDataObjectKey
|
||||
from modules.datamodels.datamodelRbac import AccessRuleContext
|
||||
objectKey = buildDataObjectKey("FileItem")
|
||||
permissions = self.rbac.getUserPermissions(
|
||||
self.currentUser, AccessRuleContext.DATA, objectKey,
|
||||
|
|
|
|||
|
|
@ -379,7 +379,6 @@ def getRecordsetWithRBAC(
|
|||
|
||||
# Handle JSONB fields and ensure numeric types are correct
|
||||
# Import the helper function from connector module
|
||||
from modules.connectors.connectorDbPostgre import getModelFields
|
||||
fields = getModelFields(modelClass)
|
||||
for record in records:
|
||||
for fieldName, fieldType in fields.items():
|
||||
|
|
@ -511,7 +510,6 @@ def getRecordsetPaginatedWithRBAC(
|
|||
whereValues.append(value)
|
||||
|
||||
if pagination and pagination.filters:
|
||||
from modules.connectors.connectorDbPostgre import getModelFields
|
||||
fields = getModelFields(modelClass)
|
||||
validColumns = set(fields.keys())
|
||||
for key, val in pagination.filters.items():
|
||||
|
|
@ -545,7 +543,6 @@ def getRecordsetPaginatedWithRBAC(
|
|||
|
||||
orderParts: List[str] = []
|
||||
if pagination and pagination.sort:
|
||||
from modules.connectors.connectorDbPostgre import getModelFields
|
||||
validColumns = set(getModelFields(modelClass).keys())
|
||||
for sf in pagination.sort:
|
||||
if sf.field in validColumns:
|
||||
|
|
@ -569,7 +566,6 @@ def getRecordsetPaginatedWithRBAC(
|
|||
cursor.execute(dataSql, whereValues)
|
||||
records = [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
from modules.connectors.connectorDbPostgre import getModelFields, parseRecordFields
|
||||
fields = getModelFields(modelClass)
|
||||
for record in records:
|
||||
parseRecordFields(record, fields, f"table {table}")
|
||||
|
|
@ -625,7 +621,6 @@ def getDistinctColumnValuesWithRBAC(
|
|||
if not connector._ensureTableExists(modelClass):
|
||||
return []
|
||||
|
||||
from modules.connectors.connectorDbPostgre import getModelFields
|
||||
fields = getModelFields(modelClass)
|
||||
if column not in fields:
|
||||
return []
|
||||
|
|
@ -949,7 +944,6 @@ def buildRbacWhereClause(
|
|||
# Fall back to Root mandate (first mandate in system) for GROUP access
|
||||
# This allows system-level tables to be accessed without explicit mandate context
|
||||
try:
|
||||
from modules.datamodels.datamodelUam import Mandate
|
||||
dbApp = getRootDbAppConnector()
|
||||
allMandates = dbApp.getRecordset(Mandate)
|
||||
if allMandates:
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ def applyViewToParams(params: Optional["PaginationParams"], viewConfig: Optional
|
|||
Returns the (mutated) params, or a new minimal PaginationParams when
|
||||
params is None (so callers always get a valid object).
|
||||
"""
|
||||
from modules.datamodels.datamodelPagination import SortField
|
||||
if not viewConfig:
|
||||
return params
|
||||
|
||||
|
|
@ -264,7 +263,6 @@ def buildGroupLayout(
|
|||
-------
|
||||
(page_items, GroupLayout | None)
|
||||
"""
|
||||
from modules.datamodels.datamodelPagination import GroupBand, GroupLayout
|
||||
|
||||
if not groupByLevels:
|
||||
offset = (page - 1) * pageSize
|
||||
|
|
|
|||
|
|
@ -473,7 +473,6 @@ def list_feature_instances(
|
|||
items = [inst.model_dump() for inst in instances]
|
||||
|
||||
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
|
||||
from modules.datamodels.datamodelFeatures import FeatureInstance
|
||||
enrichRowsWithFkLabels(items, FeatureInstance, db=rootInterface.db)
|
||||
|
||||
if mode == "filterValues":
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ def _getAdminMandateIds(context: RequestContext) -> List[str]:
|
|||
"""Get mandate IDs where the user has an admin role."""
|
||||
mandateIds = []
|
||||
try:
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
rootInterface = getRootInterface()
|
||||
userMandates = rootInterface.getUserMandates(str(context.user.id))
|
||||
for um in userMandates:
|
||||
|
|
@ -64,7 +63,6 @@ def _getAdminMandateIds(context: RequestContext) -> List[str]:
|
|||
def _isRoleInAdminMandates(roleId: str, adminMandateIds: List[str]) -> bool:
|
||||
"""Check if a role belongs to one of the admin's mandates."""
|
||||
try:
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
rootInterface = getRootInterface()
|
||||
role = rootInterface.getRole(roleId)
|
||||
if not role:
|
||||
|
|
@ -1405,7 +1403,6 @@ def cleanup_duplicate_access_rules(
|
|||
# Phase 2: Fix template role assignments
|
||||
# UserMandateRole should reference mandate-instance roles, not templates
|
||||
# =====================================================================
|
||||
from modules.datamodels.datamodelMembership import UserMandate, UserMandateRole
|
||||
|
||||
allUserMandateRoles = rootInterface.db.getRecordset(UserMandateRole)
|
||||
templateFixDetails = []
|
||||
|
|
|
|||
|
|
@ -756,7 +756,6 @@ def createOrUpdateSettings(
|
|||
return result or existingSettings
|
||||
return existingSettings
|
||||
else:
|
||||
from modules.datamodels.datamodelBilling import BillingSettings
|
||||
|
||||
newSettings = BillingSettings(
|
||||
mandateId=targetMandateId,
|
||||
|
|
@ -821,7 +820,6 @@ def addCredit(
|
|||
if creditRequest.amount == 0:
|
||||
raise HTTPException(status_code=400, detail=routeApiMsg("Amount must not be zero"))
|
||||
|
||||
from modules.datamodels.datamodelBilling import BillingTransaction
|
||||
|
||||
isDeduction = creditRequest.amount < 0
|
||||
txType = TransactionTypeEnum.DEBIT if isDeduction else TransactionTypeEnum.CREDIT
|
||||
|
|
|
|||
|
|
@ -161,7 +161,6 @@ async def get_connections(
|
|||
from modules.interfaces.interfaceTableHelpers import (
|
||||
resolveView, applyViewToParams, buildGroupLayout, effective_group_by_levels,
|
||||
)
|
||||
from modules.datamodels.datamodelPagination import AppliedViewMeta
|
||||
|
||||
CONTEXT_KEY = "connections"
|
||||
|
||||
|
|
@ -782,7 +781,6 @@ async def _updateKnowledgeConsent(
|
|||
if not connection:
|
||||
raise HTTPException(status_code=404, detail=routeApiMsg("Connection not found"))
|
||||
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
rootIf = getRootInterface()
|
||||
rootIf.db.recordModify(UserConnection, connectionId, {"knowledgeIngestionEnabled": enabled})
|
||||
|
||||
|
|
@ -861,7 +859,6 @@ def _updateKnowledgePreferences(
|
|||
cleaned = {k: v for k, v in preferences.items() if k in _ALLOWED_KEYS}
|
||||
merged = {**existing, **cleaned, "schemaVersion": 1}
|
||||
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
getRootInterface().db.recordModify(UserConnection, connectionId, {"knowledgePreferences": merged})
|
||||
|
||||
logger.info("Knowledge preferences updated for connection %s", connectionId)
|
||||
|
|
|
|||
|
|
@ -738,7 +738,6 @@ def get_files(
|
|||
resolveView, applyViewToParams, buildGroupLayout, effective_group_by_levels,
|
||||
)
|
||||
import modules.interfaces.interfaceDbApp as _appIface
|
||||
from modules.datamodels.datamodelPagination import AppliedViewMeta
|
||||
|
||||
managementInterface = interfaceDbManagement.getInterface(
|
||||
currentUser,
|
||||
|
|
@ -1202,7 +1201,6 @@ def bulk_set_neutralize(
|
|||
managementInterface.updateFile(fid, {"neutralize": neutralize})
|
||||
if not neutralize:
|
||||
try:
|
||||
from modules.interfaces import interfaceDbKnowledge
|
||||
kIface = interfaceDbKnowledge.getInterface(currentUser)
|
||||
kIface.purgeFileKnowledge(fid)
|
||||
except Exception as ke:
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ def get_prompts(
|
|||
resolveView, applyViewToParams, buildGroupLayout, effective_group_by_levels,
|
||||
)
|
||||
from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
|
||||
from modules.datamodels.datamodelPagination import AppliedViewMeta
|
||||
|
||||
CONTEXT_KEY = "prompts"
|
||||
|
||||
|
|
|
|||
|
|
@ -215,7 +215,6 @@ def mfaVerify(
|
|||
|
||||
jti = jwt.decode(accessToken, SECRET_KEY, algorithms=[ALGORITHM]).get("jti")
|
||||
|
||||
from modules.interfaces.interfaceDbApp import getInterface
|
||||
user = User.model_validate(userRecord)
|
||||
userInterface = getInterface(user)
|
||||
dbToken = Token(
|
||||
|
|
|
|||
|
|
@ -411,7 +411,6 @@ def _handleInvitationAction(
|
|||
) -> str:
|
||||
"""Handle accept/decline actions for invitation notifications."""
|
||||
from modules.datamodels.datamodelInvitation import Invitation
|
||||
from modules.datamodels.datamodelUam import Mandate
|
||||
from modules.datamodels.datamodelMembership import UserMandate
|
||||
|
||||
invitationId = notification.referenceId
|
||||
|
|
|
|||
|
|
@ -485,7 +485,6 @@ def _getInventoryPlatform(
|
|||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.interfaces.interfaceDbKnowledge import getInterface as getKnowledgeInterface
|
||||
from modules.serviceCenter.services.serviceBackgroundJobs import mainBackgroundJobService as jobService
|
||||
from modules.datamodels.datamodelUam import UserConnection
|
||||
|
||||
rootIf = getRootInterface()
|
||||
knowledgeIf = getKnowledgeInterface(None)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ def buildAuthEmailHtml(
|
|||
|
||||
operatorLine = ""
|
||||
try:
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
parts = [p for p in [
|
||||
APP_CONFIG.get("Operator_CompanyName", ""),
|
||||
APP_CONFIG.get("Operator_Address", ""),
|
||||
|
|
@ -194,7 +193,6 @@ def _ensureHomeMandate(rootInterface, user) -> None:
|
|||
return
|
||||
|
||||
try:
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIf
|
||||
appIf = _getRootIf()
|
||||
normalizedEmail = (user.email or "").strip().lower() if user.email else None
|
||||
pendingByUsername = appIf.getInvitationsByTargetUsername(user.username)
|
||||
|
|
@ -1058,7 +1056,6 @@ def _getNeutralizationMappings(
|
|||
):
|
||||
"""List the current user's neutralization placeholder mappings."""
|
||||
userId = str(context.user.id)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelFeatures import DataNeutralizerAttributes
|
||||
rootIf = getRootInterface()
|
||||
records = rootIf.db.getRecordset(DataNeutralizerAttributes, recordFilter={"userId": userId})
|
||||
|
|
@ -1074,7 +1071,6 @@ def _deleteNeutralizationMapping(
|
|||
):
|
||||
"""Delete a specific neutralization mapping owned by the current user."""
|
||||
userId = str(context.user.id)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelFeatures import DataNeutralizerAttributes
|
||||
rootIf = getRootInterface()
|
||||
records = rootIf.db.getRecordset(DataNeutralizerAttributes, recordFilter={"id": mappingId})
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ async def _listWorkflows(
|
|||
mandateId: Optional[str] = Query(default=None),
|
||||
):
|
||||
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIface
|
||||
db = _getWorkflowAutomationDb()
|
||||
try:
|
||||
db._ensureTableExists(AutoWorkflow)
|
||||
|
|
@ -174,7 +173,6 @@ async def _listRuns(
|
|||
workflowId: Optional[str] = Query(default=None),
|
||||
):
|
||||
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIface
|
||||
db = _getWorkflowAutomationDb()
|
||||
try:
|
||||
db._ensureTableExists(AutoRun)
|
||||
|
|
@ -476,17 +474,14 @@ def _listTemplates(
|
|||
templates = iface.getTemplates(scope=scope)
|
||||
|
||||
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIface
|
||||
enrichRowsWithFkLabels(templates, AutoWorkflow, db=_getRootIface().db)
|
||||
|
||||
if mode == "filterValues":
|
||||
if not column:
|
||||
raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues")
|
||||
from modules.dbHelpers.paginationHelpers import handleFilterValuesInMemory
|
||||
return handleFilterValuesInMemory(templates, column, pagination)
|
||||
|
||||
if mode == "ids":
|
||||
from modules.dbHelpers.paginationHelpers import handleIdsInMemory
|
||||
return handleIdsInMemory(templates, pagination)
|
||||
|
||||
paginationParams = None
|
||||
|
|
@ -1328,7 +1323,6 @@ def _getRunDetail(
|
|||
if tid:
|
||||
try:
|
||||
from modules.dbHelpers.fkLabelResolver import resolveInstanceLabels
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIface
|
||||
labelMap = resolveInstanceLabels(_getRootIface().db, [tid])
|
||||
targetInstanceLabel = labelMap.get(tid)
|
||||
except Exception:
|
||||
|
|
@ -1425,7 +1419,6 @@ def _startEmailPollerIfNeeded(result: dict) -> None:
|
|||
if not isinstance(result, dict) or result.get("waitReason") != "email":
|
||||
return
|
||||
try:
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.workflowAutomation.scheduler.emailPoller import ensureRunning
|
||||
root = getRootInterface()
|
||||
eventUser = root.getUserByUsername("event") if root else None
|
||||
|
|
|
|||
|
|
@ -425,7 +425,6 @@ class AgentService:
|
|||
activeToolNames.update(tb.tools)
|
||||
|
||||
from modules.serviceCenter.services.serviceAgent.externalToolRegistry import getExternalTools
|
||||
from modules.serviceCenter.services.serviceAgent.datamodelAgent import ToolDefinition
|
||||
for tb in activeToolboxes:
|
||||
extDefs = getExternalTools(tb.id)
|
||||
if not extDefs:
|
||||
|
|
@ -459,7 +458,6 @@ class AgentService:
|
|||
from modules.serviceCenter.services.serviceAgent.toolboxRegistry import (
|
||||
getToolboxRegistry, buildRequestToolboxDefinition, REQUEST_TOOLBOX_TOOL_NAME,
|
||||
)
|
||||
from modules.serviceCenter.services.serviceAgent.datamodelAgent import ToolResult
|
||||
|
||||
tbRegistry = getToolboxRegistry()
|
||||
allIds = [tb.id for tb in tbRegistry.getAllToolboxes()]
|
||||
|
|
@ -488,7 +486,6 @@ class AgentService:
|
|||
activatedCount += 1
|
||||
continue
|
||||
try:
|
||||
from modules.serviceCenter.services.serviceAgent.coreTools import registerCoreTools
|
||||
registerCoreTools(registry, self.services)
|
||||
if registry.isValidTool(toolName):
|
||||
activatedCount += 1
|
||||
|
|
@ -499,9 +496,6 @@ class AgentService:
|
|||
try:
|
||||
from modules.workflows.processing.shared.methodDiscovery import discoverMethods
|
||||
from modules.workflows.processing.core.actionExecutor import ActionExecutor
|
||||
from modules.serviceCenter.services.serviceAgent.actionToolAdapter import (
|
||||
ActionToolAdapter,
|
||||
)
|
||||
|
||||
discoverMethods(self.services)
|
||||
adapter = ActionToolAdapter(ActionExecutor(self.services))
|
||||
|
|
@ -622,7 +616,6 @@ class AgentService:
|
|||
|
||||
def _createPersistRoundMemoryFn(self, workflowId: str):
|
||||
"""Create callback that persists RoundMemory entries after tool execution."""
|
||||
from modules.serviceCenter.services.serviceAgent.agentLoop import classifyToolResult
|
||||
from modules.datamodels.datamodelKnowledge import RoundMemory
|
||||
|
||||
async def _persistRoundMemory(
|
||||
|
|
|
|||
|
|
@ -335,7 +335,6 @@ class AiService:
|
|||
Returns:
|
||||
AiCallResponse with content as JSON string (SpeechTeamsResponse format)
|
||||
"""
|
||||
from modules.datamodels.datamodelAi import AiCallResponse, AiModelCall, AiCallOptions, PriorityEnum
|
||||
|
||||
startTime = time.time()
|
||||
|
||||
|
|
@ -637,7 +636,6 @@ detectedIntent-Werte:
|
|||
try:
|
||||
from modules.aicore.aicoreModelRegistry import modelRegistry
|
||||
from modules.aicore.aicoreModelSelector import modelSelector as _modSel
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum
|
||||
|
||||
_models = modelRegistry.getAvailableModels()
|
||||
_providers = self._calculateEffectiveProviders()
|
||||
|
|
|
|||
|
|
@ -615,7 +615,6 @@ class ChatService:
|
|||
|
||||
def getUserVoicePreferences(self, userId: str, mandateId: str = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get TTS voice preferences for a user, resolved by mandate scope."""
|
||||
from modules.datamodels.datamodelUam import UserVoicePreferences
|
||||
try:
|
||||
prefRecords = self.interfaceDbApp.db.getRecordset(
|
||||
UserVoicePreferences, recordFilter={"userId": userId}
|
||||
|
|
@ -842,7 +841,6 @@ class ChatService:
|
|||
"""Create an ActionItem record in the chat DB.
|
||||
Encapsulates low-level _separateObjectFields + db.recordCreate so callers
|
||||
never need direct interfaceDbChat access."""
|
||||
from modules.datamodels.datamodelChat import ActionItem
|
||||
simpleFields, _objectFields = self.interfaceDbChat._separateObjectFields(ActionItem, actionData)
|
||||
return self.interfaceDbChat.db.recordCreate(ActionItem, simpleFields)
|
||||
|
||||
|
|
|
|||
|
|
@ -204,7 +204,6 @@ def _addFilePart(
|
|||
entryPath = f"{containerPath}/{fileName}" if containerPath else fileName
|
||||
detectedMime = _detectMimeType(fileName)
|
||||
|
||||
from ..subRegistry import getExtractorRegistry
|
||||
|
||||
registry = getExtractorRegistry()
|
||||
extractor = registry.resolve(detectedMime, fileName)
|
||||
|
|
|
|||
|
|
@ -255,7 +255,6 @@ def _delegateAttachment(attachData: bytes, attachName: str, parentId: str, depth
|
|||
guessedMime, _ = mimetypes.guess_type(attachName)
|
||||
detectedMime = guessedMime or "application/octet-stream"
|
||||
|
||||
from ..subRegistry import getExtractorRegistry
|
||||
registry = getExtractorRegistry()
|
||||
extractor = registry.resolve(detectedMime, attachName)
|
||||
|
||||
|
|
|
|||
|
|
@ -141,7 +141,6 @@ def _walkFolder(
|
|||
guessedMime, _ = mimetypes.guess_type(entry.name)
|
||||
detectedMime = guessedMime or "application/octet-stream"
|
||||
|
||||
from ..subRegistry import ExtractorRegistry
|
||||
registry = ExtractorRegistry()
|
||||
extractor = registry.resolve(detectedMime, entry.name)
|
||||
|
||||
|
|
|
|||
|
|
@ -50,8 +50,6 @@ class Extractor:
|
|||
precomputedParts: Optional[List[ContentPart]] = None,
|
||||
) -> "UdmDocument":
|
||||
"""Build UDM from extracted parts (default: heuristic grouping). Override for format-specific trees."""
|
||||
from modules.datamodels.datamodelUdm import contentPartsToUdm, mimeToUdmSourceType
|
||||
from modules.datamodels.datamodelExtraction import ContentExtracted
|
||||
from .subUtils import makeId
|
||||
|
||||
parts = precomputedParts if precomputedParts is not None else self.extract(fileBytes, context)
|
||||
|
|
|
|||
|
|
@ -230,7 +230,6 @@ class RendererPdf(BaseRenderer):
|
|||
# memory simultaneously. Collected here, deleted after the build.
|
||||
self._tempImageFiles = []
|
||||
try:
|
||||
from modules.serviceCenter.services.serviceGeneration.styleDefaults import resolveStyle
|
||||
self._unifiedStyle = unifiedStyle or resolveStyle(None)
|
||||
styles = self._convertUnifiedStyleToInternal(self._unifiedStyle)
|
||||
for level in range(1, 7):
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ class RendererPptx(BaseRenderer):
|
|||
from pptx.dml.color import RGBColor
|
||||
|
||||
if not style:
|
||||
from modules.serviceCenter.services.serviceGeneration.styleDefaults import resolveStyle
|
||||
style = resolveStyle(None)
|
||||
internalStyle = self._convertUnifiedStyleToInternal(style)
|
||||
styles = internalStyle
|
||||
|
|
|
|||
|
|
@ -137,7 +137,6 @@ class RendererXlsx(BaseRenderer):
|
|||
self.services.utils.debugLogToFile(f"EXCEL JSON CONTENT KEYS: {list(jsonContent.keys()) if isinstance(jsonContent, dict) else 'Not a dict'}", "EXCEL_RENDERER")
|
||||
|
||||
if not style:
|
||||
from modules.serviceCenter.services.serviceGeneration.styleDefaults import resolveStyle
|
||||
style = resolveStyle(None)
|
||||
self._unifiedStyle = style
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from typing import Any, Callable, Dict, List, Optional
|
|||
from modules.serviceCenter.services.serviceKnowledge.subTextClean import cleanEmailBody
|
||||
from modules.serviceCenter.services.serviceKnowledge.subWalkerHelpers import (
|
||||
WalkerTimeout,
|
||||
extractWithTimeout as _extractWithTimeout,
|
||||
ingestWithTimeout,
|
||||
logItemStart,
|
||||
)
|
||||
|
|
@ -564,10 +565,6 @@ async def _ingestAttachments(
|
|||
attLabel = f"{messageId}/att:{stub['attachmentId']}/{fileName}"
|
||||
logItemStart("gmail-attachment", attLabel, sizeBytes=stub.get("size") or None, mime=mimeType)
|
||||
|
||||
from modules.serviceCenter.services.serviceKnowledge.subWalkerHelpers import (
|
||||
extractWithTimeout as _extractWithTimeout,
|
||||
)
|
||||
|
||||
def _runAttExtraction():
|
||||
return runExtraction(
|
||||
extractorRegistry, chunkerRegistry,
|
||||
|
|
|
|||
|
|
@ -414,7 +414,6 @@ class SubscriptionService:
|
|||
|
||||
mandateLabel = mandateId
|
||||
try:
|
||||
from modules.datamodels.datamodelUam import Mandate
|
||||
from modules.security.rootAccess import getRootDbAppConnector
|
||||
appDb = getRootDbAppConnector()
|
||||
rows = appDb.getRecordset(Mandate, recordFilter={"id": mandateId})
|
||||
|
|
@ -937,7 +936,6 @@ def _buildInvoiceSummaryHtml(
|
|||
) -> str:
|
||||
"""Build an HTML invoice summary block for inclusion in the activation email."""
|
||||
import html as htmlmod
|
||||
from modules.interfaces.interfaceDbSubscription import getRootInterface as getSubRootInterface
|
||||
|
||||
subInterface = getSubRootInterface()
|
||||
userCount = subInterface.countActiveUsers(mandateId)
|
||||
|
|
|
|||
|
|
@ -805,7 +805,6 @@ def _discoverLegacyTables(dbFilter: Optional[str] = None) -> List[dict]:
|
|||
Returns a list of dicts: {db, table, rowCount, sizeBytes}.
|
||||
"""
|
||||
from modules.datamodels.datamodelBase import MODEL_REGISTRY
|
||||
from modules.dbHelpers.fkRegistry import ensureModelsLoaded
|
||||
|
||||
ensureModelsLoaded()
|
||||
registeredDbs = getRegisteredDatabases()
|
||||
|
|
@ -854,7 +853,6 @@ def _dropLegacyTable(dbName: str, tableName: str) -> dict:
|
|||
Raises ValueError if the table is model-backed (safety guard).
|
||||
"""
|
||||
from modules.datamodels.datamodelBase import MODEL_REGISTRY
|
||||
from modules.dbHelpers.fkRegistry import ensureModelsLoaded
|
||||
|
||||
ensureModelsLoaded()
|
||||
if tableName in MODEL_REGISTRY:
|
||||
|
|
|
|||
|
|
@ -305,7 +305,6 @@ def _buildConnectionRefDict(connRef: str, chatService, services) -> Optional[Dic
|
|||
|
||||
def _schemaCarriesConnectionProvenance(outputSchema: str) -> bool:
|
||||
"""True iff the port schema declares ``carriesConnectionProvenance`` in the catalog."""
|
||||
from modules.nodeCatalog.portTypes import PORT_TYPE_CATALOG
|
||||
schema = PORT_TYPE_CATALOG.get(outputSchema)
|
||||
return bool(getattr(schema, "carriesConnectionProvenance", False))
|
||||
|
||||
|
|
|
|||
|
|
@ -203,7 +203,6 @@ def _validateWorkflowAccess(
|
|||
if action == "execute":
|
||||
targetInstanceId = workflow.get("targetFeatureInstanceId")
|
||||
if targetInstanceId:
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
access = getRootInterface().getFeatureAccess(userId, targetInstanceId)
|
||||
if access and access.get("enabled"):
|
||||
return
|
||||
|
|
@ -582,7 +581,6 @@ def _getWorkflowsJoinedPaginated(
|
|||
paginationParams: PaginationParams,
|
||||
) -> dict:
|
||||
"""SQL listing: AutoWorkflow LEFT JOIN aggregated AutoRun stats (one query + count)."""
|
||||
from modules.connectors.connectorDbPostgre import getModelFields, parseRecordFields
|
||||
|
||||
wfFields = getModelFields(AutoWorkflow)
|
||||
whereClause, orderClause, limitClause, values = _buildJoinedWorkflowWhereOrderLimit(
|
||||
|
|
|
|||
|
|
@ -291,7 +291,6 @@ def onInstanceCreate(mandateId: str, instanceId: str, featureCode: str, template
|
|||
"""Create workflow instances from template definitions when a feature instance is created."""
|
||||
from modules.interfaces.interfaceWorkflowAutomation import _getWorkflowAutomationInterface
|
||||
from modules.security.rootAccess import getRootUser
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
|
||||
rootUser = getRootUser()
|
||||
waInterface = _getWorkflowAutomationInterface(rootUser, mandateId, instanceId)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ def _action_docs_to_content_parts(services, docs: List[Any]) -> List[ContentPart
|
|||
"""Extract content from ActionDocument-like objects in memory (no persistence).
|
||||
Decodes base64, runs extraction pipeline, returns ContentParts for AI.
|
||||
"""
|
||||
from modules.datamodels.datamodelExtraction import ExtractionOptions, MergeStrategy
|
||||
|
||||
all_parts = []
|
||||
extraction = services.extraction
|
||||
|
|
@ -78,7 +77,6 @@ def _resolve_file_refs_to_content_parts(services, fileIdRefs) -> List[ContentPar
|
|||
references, not chat message attachments. In the agent/chat context,
|
||||
``DocumentItemReference`` holds ChatDocument IDs that must be resolved
|
||||
via ``getChatDocumentsFromDocumentList`` instead."""
|
||||
from modules.datamodels.datamodelExtraction import ExtractionOptions, MergeStrategy
|
||||
|
||||
extraction = services.extraction
|
||||
if not extraction:
|
||||
|
|
|
|||
568
scripts/script_analyze_platform_module_graph.py
Normal file
568
scripts/script_analyze_platform_module_graph.py
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Deep platform-core module import graph analysis.
|
||||
|
||||
Output: local/notes/refernce-analysis/import-analysis-platform-modules.md
|
||||
|
||||
Usage:
|
||||
python platform-core/scripts/script_analyze_platform_module_graph.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from script_analyze_porta_imports import ( # noqa: E402
|
||||
OUTPUT_ROOT,
|
||||
PLATFORM_ROOT,
|
||||
SKIP_DIR_NAMES,
|
||||
_collectPlatformModules,
|
||||
_getPlatformContainer,
|
||||
_platformModuleId,
|
||||
_resolvePlatformImportTarget,
|
||||
_resolvePlatformRelativeImport,
|
||||
_writeText,
|
||||
)
|
||||
|
||||
|
||||
OUTPUT_FILE = OUTPUT_ROOT / "import-analysis-platform-modules.md"
|
||||
|
||||
LAYER_ORDER = {
|
||||
"shared": 0,
|
||||
"datamodels": 1,
|
||||
"connectors": 2,
|
||||
"nodeCatalog": 2,
|
||||
"dbHelpers": 3,
|
||||
"interfaces": 4,
|
||||
"system": 4,
|
||||
"security": 4,
|
||||
"auth": 4,
|
||||
"aicore": 4,
|
||||
"demoConfigs": 4,
|
||||
"serviceCenter": 5,
|
||||
"workflows": 5,
|
||||
"workflowAutomation": 5,
|
||||
"features.commcoach": 5,
|
||||
"features.neutralization": 5,
|
||||
"features.realEstate": 5,
|
||||
"features.realestate": 5,
|
||||
"features.redmine": 5,
|
||||
"features.teamsbot": 5,
|
||||
"features.trustee": 5,
|
||||
"features.workspace": 5,
|
||||
"routes": 6,
|
||||
"app": 7,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScopedImport:
|
||||
target: str
|
||||
rawModule: str
|
||||
position: str
|
||||
scope: str
|
||||
isInternal: bool
|
||||
isStdLib: bool
|
||||
|
||||
|
||||
def _shortModule(moduleId: str) -> str:
|
||||
parts = moduleId.replace("platform-core.", "").split(".")
|
||||
if len(parts) <= 3:
|
||||
return ".".join(parts)
|
||||
return ".".join(parts[-3:])
|
||||
|
||||
|
||||
LIFECYCLE_SCOPE_MARKERS = (
|
||||
"lifespan",
|
||||
"onBootstrap",
|
||||
"onStart",
|
||||
"onStop",
|
||||
"onInstanceCreate",
|
||||
"onMandateDelete",
|
||||
"registerFeature",
|
||||
"preWarm",
|
||||
)
|
||||
|
||||
|
||||
def _layerOf(moduleId: str) -> Optional[int]:
|
||||
container = _getPlatformContainer(moduleId)
|
||||
if container is None:
|
||||
return None
|
||||
return LAYER_ORDER.get(container)
|
||||
|
||||
|
||||
def _isStdLibModule(moduleName: str) -> bool:
|
||||
root = moduleName.split(".")[0]
|
||||
if root.startswith("_"):
|
||||
return False
|
||||
if root in sys.builtin_module_names:
|
||||
return True
|
||||
if hasattr(sys, "stdlib_module_names") and root in sys.stdlib_module_names:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class _DetailedImportVisitor(ast.NodeVisitor):
|
||||
def __init__(self, filePath: Path):
|
||||
self.filePath = filePath
|
||||
self.imports: List[ScopedImport] = []
|
||||
self._scopeStack: List[str] = []
|
||||
|
||||
@property
|
||||
def _currentScope(self) -> str:
|
||||
return self._scopeStack[-1] if self._scopeStack else ""
|
||||
|
||||
def _position(self) -> str:
|
||||
return "code" if self._scopeStack else "header"
|
||||
|
||||
def _add(self, rawModule: str, resolved: str, isInternal: bool) -> None:
|
||||
self.imports.append(
|
||||
ScopedImport(
|
||||
target=resolved,
|
||||
rawModule=rawModule,
|
||||
position=self._position(),
|
||||
scope=self._currentScope,
|
||||
isInternal=isInternal,
|
||||
isStdLib=_isStdLibModule(rawModule) if not isInternal else False,
|
||||
)
|
||||
)
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
||||
self._scopeStack.append(f"function {node.name}")
|
||||
self.generic_visit(node)
|
||||
self._scopeStack.pop()
|
||||
|
||||
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
||||
self._scopeStack.append(f"function {node.name}")
|
||||
self.generic_visit(node)
|
||||
self._scopeStack.pop()
|
||||
|
||||
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
||||
self._scopeStack.append(f"class {node.name}")
|
||||
self.generic_visit(node)
|
||||
self._scopeStack.pop()
|
||||
|
||||
def visit_Import(self, node: ast.Import) -> None:
|
||||
for alias in node.names:
|
||||
resolved, isInternal = _resolvePlatformImportTarget(self.filePath, alias.name)
|
||||
self._add(alias.name, resolved, isInternal)
|
||||
|
||||
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
||||
if node.level > 0:
|
||||
resolved = _resolvePlatformRelativeImport(self.filePath, node)
|
||||
if resolved:
|
||||
suffix = node.module or ""
|
||||
raw = ("." * node.level) + suffix
|
||||
self._add(raw, resolved, True)
|
||||
return
|
||||
if not node.module:
|
||||
return
|
||||
resolved, isInternal = _resolvePlatformImportTarget(self.filePath, node.module)
|
||||
self._add(node.module, resolved, isInternal)
|
||||
|
||||
|
||||
def _collectDetailedImports() -> Dict[str, List[ScopedImport]]:
|
||||
byModule: Dict[str, List[ScopedImport]] = {}
|
||||
pyFiles: List[Path] = []
|
||||
appFile = PLATFORM_ROOT / "app.py"
|
||||
if appFile.exists():
|
||||
pyFiles.append(appFile)
|
||||
for root, dirs, files in os.walk(PLATFORM_ROOT / "modules"):
|
||||
dirs[:] = [d for d in dirs if d not in SKIP_DIR_NAMES]
|
||||
for fileName in files:
|
||||
if fileName.endswith(".py"):
|
||||
pyFiles.append(Path(root) / fileName)
|
||||
|
||||
for filePath in pyFiles:
|
||||
moduleId = _platformModuleId(filePath)
|
||||
if _getPlatformContainer(moduleId) is None:
|
||||
continue
|
||||
try:
|
||||
tree = ast.parse(filePath.read_text(encoding="utf-8"), filename=str(filePath))
|
||||
except (SyntaxError, UnicodeDecodeError):
|
||||
continue
|
||||
visitor = _DetailedImportVisitor(filePath)
|
||||
visitor.visit(tree)
|
||||
byModule[moduleId] = visitor.imports
|
||||
return byModule
|
||||
|
||||
|
||||
def _internalGraph(importsByModule: Dict[str, List[ScopedImport]]) -> Dict[str, Set[str]]:
|
||||
graph: Dict[str, Set[str]] = defaultdict(set)
|
||||
for source, items in importsByModule.items():
|
||||
for item in items:
|
||||
if item.isInternal and item.target.startswith("platform-core."):
|
||||
graph[source].add(item.target)
|
||||
return dict(graph)
|
||||
|
||||
|
||||
def _mutualPairs(
|
||||
graph: Dict[str, Set[str]],
|
||||
moduleFilter: Optional[Set[str]] = None,
|
||||
) -> List[Tuple[str, str]]:
|
||||
pairs: List[Tuple[str, str]] = []
|
||||
seen: Set[Tuple[str, str]] = set()
|
||||
sources = moduleFilter if moduleFilter is not None else set(graph.keys())
|
||||
for source in sorted(sources):
|
||||
for target in graph.get(source, set()):
|
||||
if moduleFilter is not None and target not in moduleFilter:
|
||||
continue
|
||||
if target not in graph or source not in graph[target]:
|
||||
continue
|
||||
key = tuple(sorted((source, target)))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
pairs.append(key)
|
||||
return pairs
|
||||
|
||||
|
||||
def _tarjanScc(graph: Dict[str, Set[str]]) -> List[List[str]]:
|
||||
index = 0
|
||||
stack: List[str] = []
|
||||
onStack: Set[str] = set()
|
||||
indices: Dict[str, int] = {}
|
||||
lowLink: Dict[str, int] = {}
|
||||
result: List[List[str]] = []
|
||||
|
||||
nodes = set(graph.keys())
|
||||
for targets in graph.values():
|
||||
nodes.update(targets)
|
||||
|
||||
def strongConnect(node: str) -> None:
|
||||
nonlocal index
|
||||
indices[node] = index
|
||||
lowLink[node] = index
|
||||
index += 1
|
||||
stack.append(node)
|
||||
onStack.add(node)
|
||||
|
||||
for neighbor in graph.get(node, set()):
|
||||
if neighbor not in indices:
|
||||
strongConnect(neighbor)
|
||||
lowLink[node] = min(lowLink[node], lowLink[neighbor])
|
||||
elif neighbor in onStack:
|
||||
lowLink[node] = min(lowLink[node], indices[neighbor])
|
||||
|
||||
if lowLink[node] == indices[node]:
|
||||
component: List[str] = []
|
||||
while True:
|
||||
w = stack.pop()
|
||||
onStack.remove(w)
|
||||
component.append(w)
|
||||
if w == node:
|
||||
break
|
||||
if len(component) > 1 or (len(component) == 1 and component[0] in graph.get(component[0], set())):
|
||||
result.append(sorted(component))
|
||||
|
||||
for node in sorted(nodes):
|
||||
if node not in indices:
|
||||
strongConnect(node)
|
||||
return sorted(result, key=lambda c: (len(c), c[0]), reverse=True)
|
||||
|
||||
|
||||
def _canReach(graph: Dict[str, Set[str]], start: str, goal: str, skipEdge: Optional[Tuple[str, str]] = None) -> bool:
|
||||
visited: Set[str] = set()
|
||||
|
||||
def dfs(node: str) -> bool:
|
||||
if node == goal:
|
||||
return True
|
||||
if node in visited:
|
||||
return False
|
||||
visited.add(node)
|
||||
for nxt in graph.get(node, set()):
|
||||
if skipEdge and node == skipEdge[0] and nxt == skipEdge[1]:
|
||||
continue
|
||||
if dfs(nxt):
|
||||
return True
|
||||
return False
|
||||
|
||||
return dfs(start)
|
||||
|
||||
|
||||
def _assessMutualPair(a: str, b: str) -> str:
|
||||
containerA = _getPlatformContainer(a)
|
||||
containerB = _getPlatformContainer(b)
|
||||
layerA = _layerOf(a)
|
||||
layerB = _layerOf(b)
|
||||
sameContainer = containerA == containerB
|
||||
|
||||
if sameContainer:
|
||||
if layerA is not None and layerA >= 5:
|
||||
return "Prüfen — Feature/Service-interner Gegenimport; oft Lazy-Import-Workaround, Zyklus im Container."
|
||||
return "Prüfen — gegenseitiger Import im gleichen Container; meist absichtlicher Lazy-Import gegen Zyklus."
|
||||
|
||||
if layerA is not None and layerB is not None:
|
||||
if layerA < layerB and layerB < layerA:
|
||||
pass
|
||||
upward = (layerA > layerB and layerB is not None) or (layerB > layerA and layerA is not None)
|
||||
if upward:
|
||||
return "Refactor-Kandidat — untere Schicht importiert obere und umgekehrt (Layer-Verletzung)."
|
||||
return "Refactor-Kandidat — Cross-Container-Gegenimport; Layer-Grenze prüfen."
|
||||
|
||||
|
||||
def _assessCycle(component: List[str]) -> str:
|
||||
if len(component) == 1:
|
||||
return "OK — Package-Reexport/Self-Import (__init__ ↔ Submodul); typisch für Barrel-Module."
|
||||
containers = {_getPlatformContainer(m) for m in component}
|
||||
containers.discard(None)
|
||||
layers = [layer for m in component if (layer := _layerOf(m)) is not None]
|
||||
if len(containers) == 1:
|
||||
container = next(iter(containers))
|
||||
if LAYER_ORDER.get(container or "", 99) >= 5:
|
||||
return "Prüfen — Zyklus innerhalb Feature/Service-Cluster; oft bekanntes Deferred-Coupling."
|
||||
return "Prüfen — Intra-Container-Loop; Lazy-Imports prüfen ob extrahierbar."
|
||||
if layers and max(layers) - min(layers) >= 2:
|
||||
return "Refactor-Kandidat — Loop über mehrere Layer/Container; Architektur-Grenze verletzt."
|
||||
return "Prüfen — Cross-Container-Loop; Abhängigkeit entkoppeln oder Typ/Protocol extrahieren."
|
||||
|
||||
|
||||
def _assessLazyStdLib(moduleId: str, item: ScopedImport) -> str:
|
||||
heavy = {"json", "csv", "xml", "pickle", "sqlite3", "subprocess", "multiprocessing"}
|
||||
root = item.rawModule.split(".")[0]
|
||||
if root in heavy:
|
||||
return "OK — schwere Stdlib lazy (Startup/optional)."
|
||||
if "TYPE_CHECKING" in item.scope:
|
||||
return "OK — typing-only Kontext."
|
||||
return "Harmlos — Stdlib lazy in Code-Scope; kein Architektur-Risiko."
|
||||
|
||||
|
||||
def _assessMovable(moduleId: str, item: ScopedImport, graph: Dict[str, Set[str]], headerTargets: Set[str]) -> str:
|
||||
if item.target in headerTargets:
|
||||
return "Redundant — bereits im Header importiert; Lazy-Import entfernen."
|
||||
if any(marker in item.scope for marker in LIFECYCLE_SCOPE_MARKERS):
|
||||
return "Beabsichtigt lazy — Startup/Lifecycle-Hook; nicht in Header verschieben."
|
||||
if _canReach(graph, item.target, moduleId):
|
||||
return "Muss lazy bleiben — Header-Import würde Zyklus erzeugen."
|
||||
return "Verschiebbar — kann vermutlich in den Header."
|
||||
|
||||
|
||||
def _renderMarkdown(
|
||||
importsByModule: Dict[str, List[ScopedImport]],
|
||||
graph: Dict[str, Set[str]],
|
||||
) -> str:
|
||||
modulesByContainer: Dict[str, Set[str]] = defaultdict(set)
|
||||
for moduleId in importsByModule:
|
||||
container = _getPlatformContainer(moduleId)
|
||||
if container:
|
||||
modulesByContainer[container].add(moduleId)
|
||||
|
||||
lines = [
|
||||
"# Import-Analyse Platform — Modul-Graph",
|
||||
"",
|
||||
f"- **Generiert:** {date.today().isoformat()}",
|
||||
"- **Script:** `platform-core/scripts/script_analyze_platform_module_graph.py`",
|
||||
"- **Scope:** interne `modules.*`-Imports (inkl. lazy)",
|
||||
"",
|
||||
"## Legende Beurteilung",
|
||||
"",
|
||||
"| Stufe | Bedeutung |",
|
||||
"|-------|-----------|",
|
||||
"| OK / Harmlos | kein Handlungsbedarf |",
|
||||
"| Verschiebbar | Lazy-Import kann vermutlich in Header |",
|
||||
"| Redundant | doppelter Import (Header + Code) |",
|
||||
"| Prüfen | bekannt möglich, bewusst prüfen |",
|
||||
"| Beabsichtigt lazy | Startup/Lifecycle — nicht in Header |",
|
||||
"| Muss lazy bleiben | Zyklusvermeidung |",
|
||||
"| Refactor-Kandidat | Layer-/Architektur-Thema |",
|
||||
"",
|
||||
]
|
||||
|
||||
# --- Mutual pairs per container ---
|
||||
lines.extend(["## Gegenseitige Modul-Imports (Paare)", ""])
|
||||
totalPairs = 0
|
||||
for container in sorted(modulesByContainer.keys()):
|
||||
moduleSet = modulesByContainer[container]
|
||||
pairs = _mutualPairs(graph, moduleSet)
|
||||
if not pairs:
|
||||
continue
|
||||
totalPairs += len(pairs)
|
||||
lines.append(f"### Container `{container}`")
|
||||
lines.append("")
|
||||
lines.append("| Modul A | Modul B | Beurteilung |")
|
||||
lines.append("|---------|---------|-------------|")
|
||||
for a, b in pairs:
|
||||
lines.append(
|
||||
f"| `{_shortModule(a)}` | `{_shortModule(b)}` | {_assessMutualPair(a, b)} |"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
crossPairs = [
|
||||
p for p in _mutualPairs(graph)
|
||||
if _getPlatformContainer(p[0]) != _getPlatformContainer(p[1])
|
||||
]
|
||||
if crossPairs:
|
||||
lines.extend(["### Cross-Container (gegenseitig)", ""])
|
||||
lines.append("| Modul A | Container A | Modul B | Container B | Beurteilung |")
|
||||
lines.append("|---------|-------------|---------|-------------|-------------|")
|
||||
for a, b in crossPairs:
|
||||
lines.append(
|
||||
f"| `{_shortModule(a)}` | `{_getPlatformContainer(a)}` | "
|
||||
f"`{_shortModule(b)}` | `{_getPlatformContainer(b)}` | {_assessMutualPair(a, b)} |"
|
||||
)
|
||||
lines.append("")
|
||||
if totalPairs == 0 and not crossPairs:
|
||||
lines.append("_Keine gegenseitigen Modul-Paare gefunden._")
|
||||
lines.append("")
|
||||
|
||||
# --- Cycles ---
|
||||
sccList = _tarjanScc(graph)
|
||||
lines.extend(["## Import-Loops (über mehrere Module)", ""])
|
||||
if not sccList:
|
||||
lines.append("_Keine Strongly-Connected Components (>1 Knoten) gefunden._")
|
||||
lines.append("")
|
||||
else:
|
||||
lines.append(f"**{len(sccList)} Loop-Gruppe(n)** (Tarjan SCC, nur interne Module).")
|
||||
lines.append("")
|
||||
for index, component in enumerate(sccList, start=1):
|
||||
containers = sorted({c for m in component if (c := _getPlatformContainer(m))})
|
||||
lines.append(f"### Loop {index} — {len(component)} Module")
|
||||
lines.append("")
|
||||
lines.append(f"- **Container:** {', '.join(f'`{c}`' for c in containers)}")
|
||||
lines.append(f"- **Beurteilung:** {_assessCycle(component)}")
|
||||
lines.append("- **Module:**")
|
||||
for moduleId in component:
|
||||
lines.append(f" - `{moduleId}`")
|
||||
if len(component) <= 8:
|
||||
chainHint = " → ".join(_shortModule(m) for m in component) + f" → `{_shortModule(component[0])}`"
|
||||
lines.append(f"- **Ring (Auszug):** {chainHint}")
|
||||
lines.append("")
|
||||
|
||||
# --- Lazy stdlib ---
|
||||
lines.extend(["## Lazy Stdlib-Imports (in Code-Scope)", ""])
|
||||
stdlibRows: List[Tuple[str, str, str, str, str]] = []
|
||||
for moduleId, items in sorted(importsByModule.items()):
|
||||
for item in items:
|
||||
if item.position == "code" and item.isStdLib:
|
||||
stdlibRows.append(
|
||||
(
|
||||
_shortModule(moduleId),
|
||||
_getPlatformContainer(moduleId) or "",
|
||||
item.rawModule,
|
||||
item.scope or "(class/function)",
|
||||
_assessLazyStdLib(moduleId, item),
|
||||
)
|
||||
)
|
||||
if stdlibRows:
|
||||
lines.append("| Modul | Container | Import | Scope | Beurteilung |")
|
||||
lines.append("|-------|-----------|--------|-------|-------------|")
|
||||
for row in stdlibRows:
|
||||
lines.append(f"| `{row[0]}` | `{row[1]}` | `{row[2]}` | {row[3]} | {row[4]} |")
|
||||
lines.append("")
|
||||
else:
|
||||
lines.append("_Keine lazy Stdlib-Imports in Code-Scope._")
|
||||
lines.append("")
|
||||
|
||||
# --- Lazy internal movable ---
|
||||
lines.extend(["## Lazy interne Imports — Header möglich?", ""])
|
||||
movableRows: List[Tuple[str, str, str, str, str]] = []
|
||||
intentionalRows: List[Tuple[str, str, str, str, str]] = []
|
||||
mustStayRows: List[Tuple[str, str, str, str, str]] = []
|
||||
redundantRows: List[Tuple[str, str, str, str, str]] = []
|
||||
|
||||
for moduleId, items in sorted(importsByModule.items()):
|
||||
headerTargets = {i.target for i in items if i.position == "header" and i.isInternal}
|
||||
for item in items:
|
||||
if item.position != "code" or not item.isInternal:
|
||||
continue
|
||||
verdict = _assessMovable(moduleId, item, graph, headerTargets)
|
||||
row = (
|
||||
_shortModule(moduleId),
|
||||
_getPlatformContainer(moduleId) or "",
|
||||
_shortModule(item.target),
|
||||
item.scope or "(code)",
|
||||
verdict,
|
||||
)
|
||||
if verdict.startswith("Verschiebbar"):
|
||||
movableRows.append(row)
|
||||
elif verdict.startswith("Beabsichtigt"):
|
||||
intentionalRows.append(row)
|
||||
elif verdict.startswith("Redundant"):
|
||||
redundantRows.append(row)
|
||||
elif verdict.startswith("Muss lazy"):
|
||||
mustStayRows.append(row)
|
||||
|
||||
if intentionalRows:
|
||||
lines.append("### Beabsichtigt lazy (Startup/Lifecycle)")
|
||||
lines.append("")
|
||||
lines.append(f"**{len(intentionalRows)}** Einträge — lazy in lifespan/onBootstrap/…; kein Refactor nötig.")
|
||||
lines.append("")
|
||||
|
||||
if movableRows:
|
||||
lines.append("### Verschiebbar in Header")
|
||||
lines.append("")
|
||||
lines.append("| Modul | Container | Import-Ziel | Scope | Beurteilung |")
|
||||
lines.append("|-------|-----------|-------------|-------|-------------|")
|
||||
for row in movableRows:
|
||||
lines.append(f"| `{row[0]}` | `{row[1]}` | `{row[2]}` | {row[3]} | {row[4]} |")
|
||||
lines.append("")
|
||||
|
||||
if mustStayRows:
|
||||
lines.append("### Muss lazy bleiben (Zyklus)")
|
||||
lines.append("")
|
||||
lines.append(f"**{len(mustStayRows)}** Einträge — Auszug (max. 40):")
|
||||
lines.append("")
|
||||
lines.append("| Modul | Container | Import-Ziel | Scope | Beurteilung |")
|
||||
lines.append("|-------|-----------|-------------|-------|-------------|")
|
||||
for row in mustStayRows[:40]:
|
||||
lines.append(f"| `{row[0]}` | `{row[1]}` | `{row[2]}` | {row[3]} | {row[4]} |")
|
||||
if len(mustStayRows) > 40:
|
||||
lines.append("")
|
||||
lines.append(f"_… und {len(mustStayRows) - 40} weitere._")
|
||||
lines.append("")
|
||||
|
||||
if redundantRows:
|
||||
lines.append("### Redundant (Header + Code)")
|
||||
lines.append("")
|
||||
lines.append("| Modul | Container | Import-Ziel | Scope | Beurteilung |")
|
||||
lines.append("|-------|-----------|-------------|-------|-------------|")
|
||||
for row in redundantRows:
|
||||
lines.append(f"| `{row[0]}` | `{row[1]}` | `{row[2]}` | {row[3]} | {row[4]} |")
|
||||
lines.append("")
|
||||
|
||||
if not movableRows and not mustStayRows and not redundantRows and not intentionalRows:
|
||||
lines.append("_Keine lazy internen Imports gefunden._")
|
||||
lines.append("")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"## Kurzfassung",
|
||||
"",
|
||||
f"- Gegenseitige Modul-Paare (intra-container): **{totalPairs}**",
|
||||
f"- Gegenseitige Modul-Paare (cross-container): **{len(crossPairs)}**",
|
||||
f"- Import-Loop-Gruppen (SCC): **{len(sccList)}** (davon Self-Loop: **{sum(1 for c in sccList if len(c) == 1)}**)",
|
||||
f"- Lazy Stdlib-Imports: **{len(stdlibRows)}**",
|
||||
f"- Lazy intern / beabsichtigt (Lifecycle): **{len(intentionalRows)}**",
|
||||
f"- Lazy intern / verschiebbar: **{len(movableRows)}**",
|
||||
f"- Lazy intern / Zyklus (muss bleiben): **{len(mustStayRows)}**",
|
||||
f"- Lazy intern / redundant: **{len(redundantRows)}**",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Collecting detailed platform imports...")
|
||||
importsByModule = _collectDetailedImports()
|
||||
graph = _internalGraph(importsByModule)
|
||||
print(f" modules: {len(importsByModule)}")
|
||||
print(f" internal edges: {sum(len(v) for v in graph.values())}")
|
||||
|
||||
markdown = _renderMarkdown(importsByModule, graph)
|
||||
_writeText(OUTPUT_FILE, markdown)
|
||||
print(f"Written: {OUTPUT_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
898
scripts/script_analyze_porta_imports.py
Normal file
898
scripts/script_analyze_porta_imports.py
Normal file
|
|
@ -0,0 +1,898 @@
|
|||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Analyze all imports (including lazy/dynamic) for PowerOn PORTA UI and platform-core.
|
||||
|
||||
Outputs under local/notes/refernce-analysis/:
|
||||
platform/modules/*.md one file per Python module
|
||||
platform/containers/*.md aggregated stats per container
|
||||
platform/container-network.drawio
|
||||
ui/modules/*.md
|
||||
ui/containers/*.md
|
||||
ui/container-network.drawio
|
||||
README.md
|
||||
|
||||
Usage:
|
||||
python platform-core/scripts/script_analyze_porta_imports.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import html
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Optional, Set, Tuple
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
PLATFORM_ROOT = SCRIPT_DIR.parent
|
||||
REPO_ROOT = PLATFORM_ROOT.parent
|
||||
UI_ROOT = REPO_ROOT / "ui-nyla"
|
||||
OUTPUT_ROOT = REPO_ROOT / "local" / "notes" / "refernce-analysis"
|
||||
|
||||
SKIP_DIR_NAMES = {
|
||||
"__pycache__",
|
||||
"node_modules",
|
||||
".git",
|
||||
"dist",
|
||||
"build",
|
||||
".venv",
|
||||
"venv",
|
||||
".tox",
|
||||
".mypy_cache",
|
||||
".pytest_cache",
|
||||
}
|
||||
UI_SKIP_GLOBS = ("**/*.test.ts", "**/*.test.tsx", "test/**")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportRecord:
|
||||
importedModule: str
|
||||
position: str # "header" | "code"
|
||||
isInternal: bool
|
||||
sourceContainer: Optional[str] = None
|
||||
targetContainer: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleAnalysis:
|
||||
context: str # "platform" | "ui"
|
||||
moduleId: str
|
||||
filePath: Path
|
||||
container: str
|
||||
containerPath: str
|
||||
imports: List[ImportRecord] = field(default_factory=list)
|
||||
|
||||
|
||||
def _sanitizeFileName(value: str) -> str:
|
||||
return re.sub(r"[^A-Za-z0-9._-]+", "_", value)
|
||||
|
||||
|
||||
def _writeText(path: Path, content: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Platform (Python)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _platformModuleId(filePath: Path) -> str:
|
||||
rel = filePath.relative_to(PLATFORM_ROOT)
|
||||
if filePath.name == "__init__.py":
|
||||
parts = rel.parent.parts
|
||||
else:
|
||||
parts = rel.with_suffix("").parts
|
||||
return "platform-core." + ".".join(parts)
|
||||
|
||||
|
||||
def _platformContainerPath(container: str) -> str:
|
||||
if container == "app":
|
||||
return "platform-core/app.py"
|
||||
if container.startswith("features."):
|
||||
featureCode = container.split(".", 1)[1]
|
||||
return f"platform-core/modules/features/{featureCode}"
|
||||
return f"platform-core/modules/{container}"
|
||||
|
||||
|
||||
def _getPlatformContainer(moduleId: str) -> Optional[str]:
|
||||
if moduleId == "platform-core.app":
|
||||
return "app"
|
||||
|
||||
if not moduleId.startswith("platform-core."):
|
||||
return None
|
||||
|
||||
parts = moduleId.replace("platform-core.", "").split(".")
|
||||
if not parts:
|
||||
return "app"
|
||||
|
||||
if parts[0] in ("tests", "scripts") or parts[0].startswith("script_"):
|
||||
return None
|
||||
if parts[0] != "modules" or len(parts) < 2:
|
||||
return "app"
|
||||
|
||||
container = parts[1]
|
||||
if container == "features" and len(parts) > 2:
|
||||
return f"features.{parts[2]}"
|
||||
return container
|
||||
|
||||
|
||||
def _resolvePlatformRelativeImport(currentFile: Path, importNode: ast.ImportFrom) -> Optional[str]:
|
||||
dotCount = importNode.level
|
||||
moduleSuffix = importNode.module or ""
|
||||
currentDir = currentFile.parent
|
||||
|
||||
baseDir = currentDir
|
||||
for _ in range(dotCount - 1):
|
||||
baseDir = baseDir.parent
|
||||
|
||||
if moduleSuffix:
|
||||
candidate = baseDir / Path(moduleSuffix.replace(".", os.sep))
|
||||
else:
|
||||
candidate = baseDir
|
||||
|
||||
pyFile = candidate.with_suffix(".py")
|
||||
if pyFile.exists():
|
||||
return _platformModuleId(pyFile)
|
||||
|
||||
initFile = candidate / "__init__.py"
|
||||
if initFile.exists():
|
||||
return _platformModuleId(initFile)
|
||||
|
||||
rel = candidate.relative_to(PLATFORM_ROOT) if candidate.is_relative_to(PLATFORM_ROOT) else None
|
||||
if rel is None:
|
||||
return None
|
||||
return "platform-core." + ".".join(rel.with_suffix("").parts)
|
||||
|
||||
|
||||
def _resolvePlatformImportTarget(currentFile: Path, importedName: str) -> Tuple[str, bool]:
|
||||
if importedName.startswith("."):
|
||||
return importedName, False
|
||||
|
||||
if importedName.startswith("modules."):
|
||||
parts = importedName.split(".")
|
||||
checkPath = PLATFORM_ROOT
|
||||
for part in parts:
|
||||
checkPath = checkPath / part
|
||||
if checkPath.with_suffix(".py").exists():
|
||||
return _platformModuleId(checkPath.with_suffix(".py")), True
|
||||
if checkPath.is_dir() and (checkPath / "__init__.py").exists():
|
||||
return _platformModuleId(checkPath / "__init__.py"), True
|
||||
return f"platform-core.{importedName.replace('.', '.')}", True
|
||||
|
||||
return importedName, False
|
||||
|
||||
|
||||
class _PythonImportVisitor(ast.NodeVisitor):
|
||||
def __init__(self, filePath: Path):
|
||||
self.filePath = filePath
|
||||
self.imports: List[ImportRecord] = []
|
||||
self._inCodeScope = False
|
||||
|
||||
def _addImport(self, importedModule: str, isInternal: bool) -> None:
|
||||
position = "code" if self._inCodeScope else "header"
|
||||
sourceContainer = _getPlatformContainer(_platformModuleId(self.filePath))
|
||||
targetContainer = _getPlatformContainer(importedModule) if isInternal else None
|
||||
self.imports.append(
|
||||
ImportRecord(
|
||||
importedModule=importedModule,
|
||||
position=position,
|
||||
isInternal=isInternal,
|
||||
sourceContainer=sourceContainer,
|
||||
targetContainer=targetContainer,
|
||||
)
|
||||
)
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
||||
previous = self._inCodeScope
|
||||
self._inCodeScope = True
|
||||
self.generic_visit(node)
|
||||
self._inCodeScope = previous
|
||||
|
||||
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
||||
previous = self._inCodeScope
|
||||
self._inCodeScope = True
|
||||
self.generic_visit(node)
|
||||
self._inCodeScope = previous
|
||||
|
||||
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
||||
previous = self._inCodeScope
|
||||
self._inCodeScope = True
|
||||
self.generic_visit(node)
|
||||
self._inCodeScope = previous
|
||||
|
||||
def visit_Import(self, node: ast.Import) -> None:
|
||||
for alias in node.names:
|
||||
resolved, isInternal = _resolvePlatformImportTarget(self.filePath, alias.name)
|
||||
self._addImport(resolved, isInternal)
|
||||
|
||||
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
||||
if node.level > 0:
|
||||
resolved = _resolvePlatformRelativeImport(self.filePath, node)
|
||||
if resolved:
|
||||
self._addImport(resolved, True)
|
||||
else:
|
||||
suffix = node.module or ""
|
||||
display = ("." * node.level) + suffix
|
||||
self._addImport(f"(relative-unresolved) {display}", False)
|
||||
return
|
||||
|
||||
if not node.module:
|
||||
return
|
||||
|
||||
resolved, isInternal = _resolvePlatformImportTarget(self.filePath, node.module)
|
||||
self._addImport(resolved, isInternal)
|
||||
|
||||
|
||||
def _analyzePythonFile(filePath: Path) -> Optional[ModuleAnalysis]:
|
||||
container = _getPlatformContainer(_platformModuleId(filePath))
|
||||
if container is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
source = filePath.read_text(encoding="utf-8")
|
||||
tree = ast.parse(source, filename=str(filePath))
|
||||
except (SyntaxError, UnicodeDecodeError) as error:
|
||||
print(f"WARN parse failed: {filePath}: {error}")
|
||||
return None
|
||||
|
||||
visitor = _PythonImportVisitor(filePath)
|
||||
visitor.visit(tree)
|
||||
|
||||
moduleId = _platformModuleId(filePath)
|
||||
return ModuleAnalysis(
|
||||
context="platform",
|
||||
moduleId=moduleId,
|
||||
filePath=filePath,
|
||||
container=container,
|
||||
containerPath=_platformContainerPath(container),
|
||||
imports=visitor.imports,
|
||||
)
|
||||
|
||||
|
||||
def _collectPlatformModules() -> List[ModuleAnalysis]:
|
||||
modules: List[ModuleAnalysis] = []
|
||||
scanRoots = [PLATFORM_ROOT / "modules", PLATFORM_ROOT / "app.py"]
|
||||
pyFiles: List[Path] = []
|
||||
if scanRoots[1].exists():
|
||||
pyFiles.append(scanRoots[1])
|
||||
for root, dirs, files in os.walk(scanRoots[0]):
|
||||
dirs[:] = [d for d in dirs if d not in SKIP_DIR_NAMES]
|
||||
for fileName in files:
|
||||
if fileName.endswith(".py"):
|
||||
pyFiles.append(Path(root) / fileName)
|
||||
|
||||
for filePath in pyFiles:
|
||||
analysis = _analyzePythonFile(filePath)
|
||||
if analysis:
|
||||
modules.append(analysis)
|
||||
return modules
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UI (TypeScript)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
TS_IMPORT_FROM_RE = re.compile(
|
||||
r"""(?:^|\n)\s*(?:import|export)\s+(?:type\s+)?(?:[\w*\s{},\n\r]+?\sfrom\s+)?['"]([^'"]+)['"]""",
|
||||
re.MULTILINE,
|
||||
)
|
||||
TS_SIDE_EFFECT_IMPORT_RE = re.compile(
|
||||
r"""(?:^|\n)\s*import\s+['"]([^'"]+)['"]\s*;""",
|
||||
re.MULTILINE,
|
||||
)
|
||||
TS_DYNAMIC_IMPORT_RE = re.compile(r"""import\s*\(\s*['"]([^'"]+)['"]\s*\)""")
|
||||
|
||||
|
||||
def _uiModuleId(filePath: Path) -> str:
|
||||
rel = filePath.relative_to(UI_ROOT / "src")
|
||||
if filePath.name == "index.ts" or filePath.name == "index.tsx":
|
||||
parts = rel.parent.parts
|
||||
else:
|
||||
parts = rel.with_suffix("").parts
|
||||
return "ui-nyla.src." + ".".join(parts)
|
||||
|
||||
|
||||
def _uiContainerPath(container: str) -> str:
|
||||
if container.startswith("pages."):
|
||||
suffix = container.split(".", 1)[1]
|
||||
if suffix in ("admin", "basedata", "billing", "settings", "workflowAutomation"):
|
||||
return f"ui-nyla/src/pages/{suffix}"
|
||||
return f"ui-nyla/src/pages/views/{suffix}"
|
||||
if container.startswith("components."):
|
||||
suffix = container.split(".", 1)[1]
|
||||
return f"ui-nyla/src/components/{suffix}"
|
||||
return f"ui-nyla/src/{container}"
|
||||
|
||||
|
||||
def _getUiContainer(moduleId: str) -> Optional[str]:
|
||||
if not moduleId.startswith("ui-nyla.src."):
|
||||
return None
|
||||
|
||||
parts = moduleId.replace("ui-nyla.src.", "").split(".")
|
||||
if not parts:
|
||||
return None
|
||||
|
||||
top = parts[0]
|
||||
if top == "test":
|
||||
return None
|
||||
|
||||
if top == "pages":
|
||||
if len(parts) >= 3 and parts[1] == "views":
|
||||
return f"pages.{parts[2]}"
|
||||
if len(parts) >= 2:
|
||||
return f"pages.{parts[1]}"
|
||||
return "pages"
|
||||
|
||||
if top == "components" and len(parts) >= 2:
|
||||
return f"components.{parts[1]}"
|
||||
|
||||
return top
|
||||
|
||||
|
||||
def _resolveUiImport(currentFile: Path, spec: str) -> Tuple[str, bool]:
|
||||
if spec.startswith("."):
|
||||
resolvedPath = (currentFile.parent / spec).resolve()
|
||||
candidates = [
|
||||
resolvedPath,
|
||||
resolvedPath.with_suffix(".ts"),
|
||||
resolvedPath.with_suffix(".tsx"),
|
||||
resolvedPath / "index.ts",
|
||||
resolvedPath / "index.tsx",
|
||||
]
|
||||
for candidate in candidates:
|
||||
if candidate.exists() and candidate.is_relative_to(UI_ROOT / "src"):
|
||||
return _uiModuleId(candidate), True
|
||||
relDisplay = spec
|
||||
return relDisplay, False
|
||||
|
||||
return spec, False
|
||||
|
||||
|
||||
def _findTsImportPosition(source: str, matchStart: int) -> str:
|
||||
depth = 0
|
||||
inFunction = False
|
||||
functionDepth = 0
|
||||
i = 0
|
||||
while i < matchStart:
|
||||
char = source[i]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth = max(0, depth - 1)
|
||||
if inFunction and depth < functionDepth:
|
||||
inFunction = False
|
||||
i += 1
|
||||
|
||||
lookback = source[max(0, matchStart - 400):matchStart]
|
||||
if re.search(r"(?:function\s*\w*\s*\(|=>\s*\{|(?:async\s+)?function\s+\w+\s*\()", lookback):
|
||||
tail = lookback[lookback.rfind("\n") + 1:]
|
||||
if "=>" in tail or "function" in tail:
|
||||
bracePos = source.find("{", max(0, matchStart - 120), matchStart)
|
||||
if bracePos >= 0:
|
||||
return "code"
|
||||
|
||||
return "header" if depth == 0 and not inFunction else "code"
|
||||
|
||||
|
||||
def _analyzeTypeScriptFile(filePath: Path) -> Optional[ModuleAnalysis]:
|
||||
container = _getUiContainer(_uiModuleId(filePath))
|
||||
if container is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
source = filePath.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError as error:
|
||||
print(f"WARN read failed: {filePath}: {error}")
|
||||
return None
|
||||
|
||||
imports: List[ImportRecord] = []
|
||||
seen: Set[Tuple[str, str, str]] = set()
|
||||
|
||||
def _register(spec: str, position: str) -> None:
|
||||
resolved, isInternal = _resolveUiImport(filePath, spec)
|
||||
key = (resolved, position, spec)
|
||||
if key in seen:
|
||||
return
|
||||
seen.add(key)
|
||||
sourceContainer = container
|
||||
targetContainer = _getUiContainer(resolved) if isInternal else None
|
||||
imports.append(
|
||||
ImportRecord(
|
||||
importedModule=resolved,
|
||||
position=position,
|
||||
isInternal=isInternal,
|
||||
sourceContainer=sourceContainer,
|
||||
targetContainer=targetContainer,
|
||||
)
|
||||
)
|
||||
|
||||
for match in TS_IMPORT_FROM_RE.finditer(source):
|
||||
position = _findTsImportPosition(source, match.start())
|
||||
_register(match.group(1), position)
|
||||
|
||||
for match in TS_SIDE_EFFECT_IMPORT_RE.finditer(source):
|
||||
if match.group(1) in {m.group(1) for m in TS_IMPORT_FROM_RE.finditer(source)}:
|
||||
continue
|
||||
position = _findTsImportPosition(source, match.start())
|
||||
_register(match.group(1), position)
|
||||
|
||||
for match in TS_DYNAMIC_IMPORT_RE.finditer(source):
|
||||
position = _findTsImportPosition(source, match.start())
|
||||
_register(match.group(1), position)
|
||||
|
||||
moduleId = _uiModuleId(filePath)
|
||||
return ModuleAnalysis(
|
||||
context="ui",
|
||||
moduleId=moduleId,
|
||||
filePath=filePath,
|
||||
container=container,
|
||||
containerPath=_uiContainerPath(container),
|
||||
imports=imports,
|
||||
)
|
||||
|
||||
|
||||
def _collectUiModules() -> List[ModuleAnalysis]:
|
||||
srcRoot = UI_ROOT / "src"
|
||||
modules: List[ModuleAnalysis] = []
|
||||
for filePath in srcRoot.rglob("*"):
|
||||
if not filePath.is_file():
|
||||
continue
|
||||
if filePath.suffix not in (".ts", ".tsx"):
|
||||
continue
|
||||
rel = filePath.relative_to(srcRoot).as_posix()
|
||||
if rel.startswith("test/") or rel.endswith(".test.ts") or rel.endswith(".test.tsx"):
|
||||
continue
|
||||
analysis = _analyzeTypeScriptFile(filePath)
|
||||
if analysis:
|
||||
modules.append(analysis)
|
||||
return modules
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Markdown output
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _renderModuleMarkdown(module: ModuleAnalysis) -> str:
|
||||
lines = [
|
||||
f"# Module Import Analysis: `{module.moduleId}`",
|
||||
"",
|
||||
f"- **Kontext:** {module.context}",
|
||||
f"- **Container:** `{module.container}`",
|
||||
f"- **Container-Pfad:** `{module.containerPath}`",
|
||||
f"- **Datei:** `{module.filePath.relative_to(REPO_ROOT).as_posix()}`",
|
||||
f"- **Import-Anzahl:** {len(module.imports)}",
|
||||
"",
|
||||
"## Imports",
|
||||
"",
|
||||
"| Modul | Position | Intern |",
|
||||
"|-------|----------|--------|",
|
||||
]
|
||||
|
||||
for item in sorted(module.imports, key=lambda x: (x.importedModule, x.position)):
|
||||
internal = "ja" if item.isInternal else "nein"
|
||||
lines.append(f"| `{item.importedModule}` | {item.position} | {internal} |")
|
||||
|
||||
if not module.imports:
|
||||
lines.append("| _keine_ | | |")
|
||||
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContainerStats:
|
||||
container: str
|
||||
containerPath: str
|
||||
importsFrom: Dict[str, int] = field(default_factory=lambda: defaultdict(int))
|
||||
exportedTo: Dict[str, int] = field(default_factory=lambda: defaultdict(int))
|
||||
mixedWith: Dict[str, Tuple[int, int]] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _buildContainerStats(modules: Iterable[ModuleAnalysis]) -> Dict[str, ContainerStats]:
|
||||
statsByContainer: Dict[str, ContainerStats] = {}
|
||||
|
||||
for module in modules:
|
||||
if module.container not in statsByContainer:
|
||||
statsByContainer[module.container] = ContainerStats(
|
||||
container=module.container,
|
||||
containerPath=module.containerPath,
|
||||
)
|
||||
|
||||
for item in module.imports:
|
||||
if not item.isInternal:
|
||||
continue
|
||||
if not item.sourceContainer or not item.targetContainer:
|
||||
continue
|
||||
if item.sourceContainer == item.targetContainer:
|
||||
continue
|
||||
|
||||
stats = statsByContainer[item.sourceContainer]
|
||||
stats.importsFrom[item.targetContainer] += 1
|
||||
|
||||
targetStats = statsByContainer.get(item.targetContainer)
|
||||
if targetStats is None:
|
||||
targetStats = ContainerStats(
|
||||
container=item.targetContainer,
|
||||
containerPath=_platformContainerPath(item.targetContainer)
|
||||
if module.context == "platform"
|
||||
else _uiContainerPath(item.targetContainer),
|
||||
)
|
||||
statsByContainer[item.targetContainer] = targetStats
|
||||
targetStats.exportedTo[item.sourceContainer] += 1
|
||||
|
||||
for containerName, stats in statsByContainer.items():
|
||||
mixed: Dict[str, Tuple[int, int]] = {}
|
||||
for other, outCount in stats.importsFrom.items():
|
||||
inCount = stats.exportedTo.get(other, 0)
|
||||
if inCount > 0:
|
||||
mixed[other] = (outCount, inCount)
|
||||
stats.mixedWith = mixed
|
||||
|
||||
return statsByContainer
|
||||
|
||||
|
||||
def _renderContainerMarkdown(context: str, stats: ContainerStats) -> str:
|
||||
importsTotal = sum(stats.importsFrom.values())
|
||||
exportsTotal = sum(stats.exportedTo.values())
|
||||
mixedTotal = sum(min(pair[0], pair[1]) for pair in stats.mixedWith.values())
|
||||
|
||||
lines = [
|
||||
f"# Container Import Analysis: `{stats.container}`",
|
||||
"",
|
||||
f"- **Kontext:** {context}",
|
||||
f"- **Container-Pfad:** `{stats.containerPath}`",
|
||||
"",
|
||||
"## Imports aus anderen Containern",
|
||||
"",
|
||||
f"- **Anzahl:** {importsTotal}",
|
||||
f"- **Container ({len(stats.importsFrom)}):** "
|
||||
+ (", ".join(f"`{name}` ({count})" for name, count in sorted(stats.importsFrom.items())) or "_keine_"),
|
||||
"",
|
||||
"## Exports zu anderen Containern",
|
||||
"",
|
||||
f"- **Anzahl:** {exportsTotal}",
|
||||
f"- **Container ({len(stats.exportedTo)}):** "
|
||||
+ (", ".join(f"`{name}` ({count})" for name, count in sorted(stats.exportedTo.items())) or "_keine_"),
|
||||
"",
|
||||
"## Cross (mixed Import/Export)",
|
||||
"",
|
||||
f"- **Anzahl bidirektionaler Paare:** {len(stats.mixedWith)}",
|
||||
f"- **Mindest-Wechselzahl (min je Richtung):** {mixedTotal}",
|
||||
]
|
||||
|
||||
if stats.mixedWith:
|
||||
lines.extend(["", "| Container | Importe hinaus | Importe herein |", "|-----------|----------------|----------------|"])
|
||||
for other, (outCount, inCount) in sorted(stats.mixedWith.items()):
|
||||
lines.append(f"| `{other}` | {outCount} | {inCount} |")
|
||||
else:
|
||||
lines.append("- **Container:** _keine_")
|
||||
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _renderReadme(platformModules: int, uiModules: int, platformContainers: int, uiContainers: int) -> str:
|
||||
return f"""# PORTA Import-Analyse
|
||||
|
||||
Generiert am {date.today().isoformat()} durch `platform-core/scripts/script_analyze_porta_imports.py`.
|
||||
|
||||
## Umfang
|
||||
|
||||
| Kontext | Module | Container |
|
||||
|---------|--------|-----------|
|
||||
| platform | {platformModules} | {platformContainers} |
|
||||
| ui | {uiModules} | {uiContainers} |
|
||||
|
||||
## Struktur
|
||||
|
||||
- `import-analysis-platform.md` — konsolidierte Platform-Übersicht (Tabelle)
|
||||
- `import-analysis-platform-modules.md` — Modul-Graph: Gegenimporte, Loops, lazy Imports
|
||||
- `import-analysis-ui.md` — konsolidierte UI-Übersicht (Tabelle)
|
||||
- `platform/modules/` — ein Markdown pro Python-Modul (alle Imports inkl. lazy)
|
||||
- `platform/containers/` — aggregierte Container-Statistik
|
||||
- `platform/container-network.drawio` — Container-Vernetzung (schwarz=einweg, rot=mixed)
|
||||
- `ui/modules/` — ein Markdown pro TS/TSX-Modul
|
||||
- `ui/containers/` — aggregierte Container-Statistik
|
||||
- `ui/container-network.drawio` — Container-Vernetzung
|
||||
- `container-network.drawio` — kombiniert (2 Diagramm-Tabs: platform + ui)
|
||||
|
||||
## Position
|
||||
|
||||
- `header` — Import auf Modulebene (Top-Level)
|
||||
- `code` — Import innerhalb von Funktion/Klasse oder dynamisch (`import()`)
|
||||
|
||||
## Regenerieren
|
||||
|
||||
```bash
|
||||
python platform-core/scripts/script_analyze_porta_imports.py
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# draw.io
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CONTAINER_COLORS = {
|
||||
"app": "#dae8fc",
|
||||
"aicore": "#d5e8d4",
|
||||
"auth": "#ffe6cc",
|
||||
"connectors": "#e1d5e7",
|
||||
"datamodels": "#fff2cc",
|
||||
"interfaces": "#f8cecc",
|
||||
"routes": "#d0cee2",
|
||||
"security": "#fad7ac",
|
||||
"serviceCenter": "#b1ddf0",
|
||||
"shared": "#f0fff0",
|
||||
"workflows": "#f5f5f5",
|
||||
"workflowAutomation": "#e6d0de",
|
||||
"system": "#cce5ff",
|
||||
"dbHelpers": "#fff0f5",
|
||||
"nodeCatalog": "#f5fffa",
|
||||
"pages": "#dae8fc",
|
||||
"components": "#d5e8d4",
|
||||
"hooks": "#ffe6cc",
|
||||
"contexts": "#e1d5e7",
|
||||
"api": "#fff2cc",
|
||||
"layouts": "#f8cecc",
|
||||
"providers": "#d0cee2",
|
||||
"config": "#fad7ac",
|
||||
"utils": "#b1ddf0",
|
||||
"types": "#f0fff0",
|
||||
"locales": "#f5f5f5",
|
||||
"stores": "#e2efda",
|
||||
"styles": "#fce5cd",
|
||||
}
|
||||
|
||||
|
||||
def _aggregateContainerEdges(statsByContainer: Dict[str, ContainerStats]) -> Dict[Tuple[str, str], Tuple[int, int, bool]]:
|
||||
pairCounts: Dict[Tuple[str, str], Tuple[int, int]] = {}
|
||||
|
||||
for stats in statsByContainer.values():
|
||||
for target, count in stats.importsFrom.items():
|
||||
key = (stats.container, target)
|
||||
outCount, inCount = pairCounts.get(key, (0, 0))
|
||||
pairCounts[key] = (outCount + count, inCount)
|
||||
|
||||
edges: Dict[Tuple[str, str], Tuple[int, int, bool]] = {}
|
||||
processed: Set[Tuple[str, str]] = set()
|
||||
|
||||
for (source, target), (forward, _) in list(pairCounts.items()):
|
||||
pairKey = tuple(sorted((source, target)))
|
||||
if pairKey in processed:
|
||||
continue
|
||||
processed.add(pairKey)
|
||||
|
||||
a, b = pairKey
|
||||
aToB = pairCounts.get((a, b), (0, 0))[0]
|
||||
bToA = pairCounts.get((b, a), (0, 0))[0]
|
||||
|
||||
if aToB == 0 and bToA == 0:
|
||||
continue
|
||||
|
||||
if aToB > 0 and bToA > 0:
|
||||
edges[(a, b)] = (aToB, bToA, True)
|
||||
elif aToB > 0:
|
||||
edges[(a, b)] = (aToB, 0, False)
|
||||
else:
|
||||
edges[(b, a)] = (bToA, 0, False)
|
||||
|
||||
return edges
|
||||
|
||||
|
||||
def _generateDrawio(context: str, statsByContainer: Dict[str, ContainerStats]) -> str:
|
||||
containers = sorted(statsByContainer.keys())
|
||||
edges = _aggregateContainerEdges(statsByContainer)
|
||||
|
||||
centerX = 700
|
||||
centerY = 550
|
||||
radius = 430
|
||||
nodeWidth = 170
|
||||
nodeHeight = 62
|
||||
|
||||
containerPositions: Dict[str, Tuple[int, int]] = {}
|
||||
for index, container in enumerate(containers):
|
||||
angle = (2 * math.pi * index / max(len(containers), 1)) - math.pi / 2
|
||||
x = int(centerX + radius * math.cos(angle) - nodeWidth / 2)
|
||||
y = int(centerY + radius * math.sin(angle) - nodeHeight / 2)
|
||||
containerPositions[container] = (x, y)
|
||||
|
||||
cells: List[str] = []
|
||||
for container in containers:
|
||||
x, y = containerPositions[container]
|
||||
base = container.split(".")[0]
|
||||
color = CONTAINER_COLORS.get(base, "#ffffff")
|
||||
label = f"{container}\\n({sum(statsByContainer[container].importsFrom.values())} out / "
|
||||
label += f"{sum(statsByContainer[container].exportedTo.values())} in)"
|
||||
cellId = f"container_{container.replace('.', '_')}"
|
||||
cells.append(
|
||||
f""" <mxCell id="{cellId}" value="{html.escape(label)}" """
|
||||
f"""style="rounded=1;whiteSpace=wrap;html=1;fillColor={color};strokeColor=#666666;fontStyle=1;fontSize=11;" """
|
||||
f"""vertex="1" parent="1">
|
||||
<mxGeometry x="{x}" y="{y}" width="{nodeWidth}" height="{nodeHeight}" as="geometry" />
|
||||
</mxCell>"""
|
||||
)
|
||||
|
||||
edgeId = 1000
|
||||
for (source, target), (forward, backward, isMixed) in sorted(edges.items(), key=lambda item: -(item[1][0] + item[1][1])):
|
||||
sourceId = f"container_{source.replace('.', '_')}"
|
||||
targetId = f"container_{target.replace('.', '_')}"
|
||||
if isMixed:
|
||||
label = f"{forward} / {backward}"
|
||||
strokeColor = "#CC0000"
|
||||
else:
|
||||
label = str(forward)
|
||||
strokeColor = "#000000"
|
||||
|
||||
strokeWidth = min(1 + (forward + backward) // 15, 6)
|
||||
cells.append(
|
||||
f""" <mxCell id="edge_{edgeId}" value="{html.escape(label)}" """
|
||||
f"""style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;"""
|
||||
f"""endArrow=block;endFill=1;strokeWidth={strokeWidth};strokeColor={strokeColor};"""
|
||||
f"""fontSize=10;labelBackgroundColor=#ffffff;fontStyle=1;" """
|
||||
f"""edge="1" parent="1" source="{sourceId}" target="{targetId}">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>"""
|
||||
)
|
||||
edgeId += 1
|
||||
|
||||
innerXml = f""" <mxGraphModel dx="1434" dy="780" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1200" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
{chr(10).join(cells)}
|
||||
</root>
|
||||
</mxGraphModel>"""
|
||||
|
||||
return _wrapDrawioDiagram(context, innerXml)
|
||||
|
||||
|
||||
def _wrapDrawioDiagram(context: str, innerXml: str) -> str:
|
||||
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="{date.today().isoformat()}T00:00:00.000Z" agent="script_analyze_porta_imports.py" version="21.0.0" type="device">
|
||||
<diagram id="{context}-container-network" name="{context} container imports">
|
||||
{innerXml}
|
||||
</diagram>
|
||||
</mxfile>
|
||||
"""
|
||||
|
||||
|
||||
def _extractDrawioDiagramBody(drawioXml: str) -> str:
|
||||
start = drawioXml.index("<mxGraphModel")
|
||||
end = drawioXml.index("</diagram>")
|
||||
return drawioXml[start:end]
|
||||
|
||||
|
||||
def _combineDrawioFiles(platformDrawio: str, uiDrawio: str) -> str:
|
||||
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="{date.today().isoformat()}T00:00:00.000Z" agent="script_analyze_porta_imports.py" version="21.0.0" type="device">
|
||||
<diagram id="platform-container-network" name="platform container imports">
|
||||
{_extractDrawioDiagramBody(platformDrawio)}
|
||||
</diagram>
|
||||
<diagram id="ui-container-network" name="ui container imports">
|
||||
{_extractDrawioDiagramBody(uiDrawio)}
|
||||
</diagram>
|
||||
</mxfile>
|
||||
"""
|
||||
|
||||
|
||||
SUMMARY_FILE_PLATFORM = "import-analysis-platform.md"
|
||||
SUMMARY_FILE_UI = "import-analysis-ui.md"
|
||||
|
||||
|
||||
def _renderConsolidatedSummary(
|
||||
title: str,
|
||||
context: str,
|
||||
detailFolder: str,
|
||||
statsByContainer: Dict[str, ContainerStats],
|
||||
diagramPath: str,
|
||||
) -> str:
|
||||
lines = [
|
||||
f"# {title}",
|
||||
"",
|
||||
f"- **Kontext:** {context}",
|
||||
f"- **Generiert:** {date.today().isoformat()}",
|
||||
f"- **Detail-Dateien:** `{detailFolder}/`",
|
||||
"",
|
||||
"## Container",
|
||||
"",
|
||||
"| Container | Imports out | Exports in | Mixed | Detail |",
|
||||
"|-----------|------------:|-----------:|------:|--------|",
|
||||
]
|
||||
|
||||
for containerName in sorted(statsByContainer.keys()):
|
||||
stats = statsByContainer[containerName]
|
||||
importsOut = sum(stats.importsFrom.values())
|
||||
exportsIn = sum(stats.exportedTo.values())
|
||||
mixedCount = len(stats.mixedWith)
|
||||
detailLink = f"[Detail]({detailFolder}/containers/{_sanitizeFileName(containerName)}.md)"
|
||||
lines.append(
|
||||
f"| `{containerName}` | {importsOut} | {exportsIn} | {mixedCount} | {detailLink} |"
|
||||
)
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"Diagramm: [{diagramPath}]({diagramPath})",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _writeContextOutput(context: str, modules: List[ModuleAnalysis]) -> Tuple[int, str]:
|
||||
contextRoot = OUTPUT_ROOT / context
|
||||
modulesDir = contextRoot / "modules"
|
||||
containersDir = contextRoot / "containers"
|
||||
|
||||
for module in modules:
|
||||
fileName = _sanitizeFileName(module.moduleId) + ".md"
|
||||
_writeText(modulesDir / fileName, _renderModuleMarkdown(module))
|
||||
|
||||
statsByContainer = _buildContainerStats(modules)
|
||||
for containerName, stats in sorted(statsByContainer.items()):
|
||||
fileName = _sanitizeFileName(containerName) + ".md"
|
||||
_writeText(containersDir / fileName, _renderContainerMarkdown(context, stats))
|
||||
|
||||
drawio = _generateDrawio(context, statsByContainer)
|
||||
_writeText(contextRoot / "container-network.drawio", drawio)
|
||||
|
||||
return len(statsByContainer), drawio
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Analyzing platform-core (Python)...")
|
||||
platformModules = _collectPlatformModules()
|
||||
print(f" modules: {len(platformModules)}")
|
||||
|
||||
print("Analyzing ui-nyla (TypeScript)...")
|
||||
uiModules = _collectUiModules()
|
||||
print(f" modules: {len(uiModules)}")
|
||||
|
||||
platformContainerCount, platformDrawio = _writeContextOutput("platform", platformModules)
|
||||
uiContainerCount, uiDrawio = _writeContextOutput("ui", uiModules)
|
||||
|
||||
combinedDrawio = _combineDrawioFiles(platformDrawio, uiDrawio)
|
||||
_writeText(OUTPUT_ROOT / "container-network.drawio", combinedDrawio)
|
||||
|
||||
readme = _renderReadme(
|
||||
platformModules=len(platformModules),
|
||||
uiModules=len(uiModules),
|
||||
platformContainers=platformContainerCount,
|
||||
uiContainers=uiContainerCount,
|
||||
)
|
||||
_writeText(OUTPUT_ROOT / "README.md", readme)
|
||||
|
||||
platformStats = _buildContainerStats(platformModules)
|
||||
platformSummary = _renderConsolidatedSummary(
|
||||
title="Import-Analyse Platform Core",
|
||||
context="platform",
|
||||
detailFolder="platform",
|
||||
statsByContainer=platformStats,
|
||||
diagramPath="platform/container-network.drawio",
|
||||
)
|
||||
_writeText(OUTPUT_ROOT / SUMMARY_FILE_PLATFORM, platformSummary)
|
||||
|
||||
uiStats = _buildContainerStats(uiModules)
|
||||
uiSummary = _renderConsolidatedSummary(
|
||||
title="Import-Analyse UI Nyla",
|
||||
context="ui",
|
||||
detailFolder="ui",
|
||||
statsByContainer=uiStats,
|
||||
diagramPath="ui/container-network.drawio",
|
||||
)
|
||||
_writeText(OUTPUT_ROOT / SUMMARY_FILE_UI, uiSummary)
|
||||
|
||||
print(f"\nOutput written to: {OUTPUT_ROOT}")
|
||||
print(f" platform containers: {platformContainerCount}")
|
||||
print(f" ui containers: {uiContainerCount}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
165
scripts/script_remove_redundant_platform_imports.py
Normal file
165
scripts/script_remove_redundant_platform_imports.py
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# Copyright (c) 2026 PowerOn AG
|
||||
# All rights reserved.
|
||||
"""
|
||||
Remove redundant lazy imports in platform-core when the same internal module
|
||||
is already imported at module header level.
|
||||
|
||||
Usage:
|
||||
python platform-core/scripts/script_remove_redundant_platform_imports.py
|
||||
python platform-core/scripts/script_remove_redundant_platform_imports.py --dry-run
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Set, Tuple
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from script_analyze_porta_imports import ( # noqa: E402
|
||||
PLATFORM_ROOT,
|
||||
SKIP_DIR_NAMES,
|
||||
_getPlatformContainer,
|
||||
_platformModuleId,
|
||||
_resolvePlatformImportTarget,
|
||||
_resolvePlatformRelativeImport,
|
||||
)
|
||||
|
||||
|
||||
class _RedundantImportFinder(ast.NodeVisitor):
|
||||
def __init__(self, filePath: Path):
|
||||
self.filePath = filePath
|
||||
self.headerTargets: Set[str] = set()
|
||||
self.linesToRemove: Set[int] = set()
|
||||
self._scopeDepth = 0
|
||||
|
||||
def _resolveImportNode(self, node: ast.Import | ast.ImportFrom) -> List[Tuple[str, bool]]:
|
||||
resolved: List[Tuple[str, bool]] = []
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
if node.level > 0:
|
||||
target = _resolvePlatformRelativeImport(self.filePath, node)
|
||||
if target:
|
||||
resolved.append((target, True))
|
||||
return resolved
|
||||
if not node.module:
|
||||
return resolved
|
||||
target, isInternal = _resolvePlatformImportTarget(self.filePath, node.module)
|
||||
resolved.append((target, isInternal))
|
||||
return resolved
|
||||
|
||||
for alias in node.names:
|
||||
target, isInternal = _resolvePlatformImportTarget(self.filePath, alias.name)
|
||||
resolved.append((target, isInternal))
|
||||
return resolved
|
||||
|
||||
def _handleImportNode(self, node: ast.Import | ast.ImportFrom) -> None:
|
||||
for target, isInternal in self._resolveImportNode(node):
|
||||
if not isInternal or not target.startswith("platform-core."):
|
||||
continue
|
||||
if self._scopeDepth == 0:
|
||||
self.headerTargets.add(target)
|
||||
elif target in self.headerTargets:
|
||||
endLine = getattr(node, "end_lineno", None) or node.lineno
|
||||
for lineNo in range(node.lineno, endLine + 1):
|
||||
self.linesToRemove.add(lineNo)
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
||||
self._scopeDepth += 1
|
||||
self.generic_visit(node)
|
||||
self._scopeDepth -= 1
|
||||
|
||||
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
||||
self._scopeDepth += 1
|
||||
self.generic_visit(node)
|
||||
self._scopeDepth -= 1
|
||||
|
||||
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
||||
self._scopeDepth += 1
|
||||
self.generic_visit(node)
|
||||
self._scopeDepth -= 1
|
||||
|
||||
def visit_Import(self, node: ast.Import) -> None:
|
||||
self._handleImportNode(node)
|
||||
|
||||
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
||||
self._handleImportNode(node)
|
||||
|
||||
|
||||
def _moduleIdToFilePath(moduleId: str) -> Path:
|
||||
rel = moduleId.replace("platform-core.", "")
|
||||
parts = rel.split(".")
|
||||
candidate = PLATFORM_ROOT.joinpath(*parts).with_suffix(".py")
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
initFile = PLATFORM_ROOT.joinpath(*parts, "__init__.py")
|
||||
return initFile
|
||||
|
||||
|
||||
def _collectPythonFiles() -> List[Path]:
|
||||
pyFiles: List[Path] = []
|
||||
appFile = PLATFORM_ROOT / "app.py"
|
||||
if appFile.exists():
|
||||
pyFiles.append(appFile)
|
||||
for root, dirs, files in os.walk(PLATFORM_ROOT / "modules"):
|
||||
dirs[:] = [d for d in dirs if d not in SKIP_DIR_NAMES]
|
||||
for fileName in files:
|
||||
if fileName.endswith(".py"):
|
||||
pyFiles.append(Path(root) / fileName)
|
||||
return pyFiles
|
||||
|
||||
|
||||
def _removeLines(filePath: Path, linesToRemove: Set[int], dryRun: bool) -> int:
|
||||
if not linesToRemove:
|
||||
return 0
|
||||
|
||||
lines = filePath.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
newLines = [line for index, line in enumerate(lines, start=1) if index not in linesToRemove]
|
||||
|
||||
if not dryRun:
|
||||
filePath.write_text("".join(newLines), encoding="utf-8")
|
||||
return len(linesToRemove)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
totalRemoved = 0
|
||||
filesChanged = 0
|
||||
details: List[Tuple[str, int]] = []
|
||||
|
||||
for filePath in _collectPythonFiles():
|
||||
moduleId = _platformModuleId(filePath)
|
||||
if _getPlatformContainer(moduleId) is None:
|
||||
continue
|
||||
try:
|
||||
tree = ast.parse(filePath.read_text(encoding="utf-8"), filename=str(filePath))
|
||||
except (SyntaxError, UnicodeDecodeError) as error:
|
||||
print(f"WARN skip {filePath}: {error}")
|
||||
continue
|
||||
|
||||
finder = _RedundantImportFinder(filePath)
|
||||
finder.visit(tree)
|
||||
if not finder.linesToRemove:
|
||||
continue
|
||||
|
||||
removed = _removeLines(filePath, finder.linesToRemove, args.dry_run)
|
||||
totalRemoved += removed
|
||||
filesChanged += 1
|
||||
rel = filePath.relative_to(PLATFORM_ROOT.parent).as_posix()
|
||||
details.append((rel, removed))
|
||||
action = "would remove" if args.dry_run else "removed"
|
||||
print(f"{action} {removed} from {rel}")
|
||||
|
||||
print(f"\nFiles touched: {filesChanged}")
|
||||
print(f"Import lines {('would be ' if args.dry_run else '')}removed: {totalRemoved}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue