Files
LinkScope_Client/Core/ResolutionManager.py
2022-09-26 22:07:39 +03:00

206 lines
9.6 KiB
Python

#!/usr/bin/env python3
import importlib.util
import sys
from os import listdir
from pathlib import Path
from uuid import uuid4
from typing import Union
from shutil import move
from msgpack import dump, load
class ResolutionManager:
# Load all resources needed.
def __init__(self, mainWindow, messageHandler):
self.messageHandler = messageHandler
self.mainWindow = mainWindow
self.resolutions = {}
# Macro dict item contents: tuple of (resolution, parameter values)
self.macros = {}
def loadResolutionsFromDir(self, directory: Path) -> None:
exceptionsCount = 0
for resolution in listdir(directory):
resolution = str(resolution)
resolutionCategory = "Uncategorized"
try:
if resolution.endswith('.py'):
resolutionName = resolution[:-3]
spec = importlib.util.spec_from_file_location(
resolutionName,
directory / resolution)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
resClass = getattr(module, resolutionName)
resClassInst = resClass()
resNameString = resClassInst.name.replace('_', ' ')
resolutionDesc = resClassInst.description
originTypes = resClassInst.originTypes
resultTypes = resClassInst.resultTypes
resolutionParameters = resClassInst.parameters
try:
resolutionCategory = resClassInst.category
if not isinstance(resolutionCategory, str):
raise AttributeError()
except AttributeError:
pass
if self.resolutions.get(resolutionCategory) is None:
self.resolutions[resolutionCategory] = {}
self.resolutions[resolutionCategory][resNameString] = {'name': resNameString,
'description': resolutionDesc,
'originTypes': originTypes,
'resultTypes': resultTypes,
'parameters': resolutionParameters,
'category': resolutionCategory,
'resolution': resClass
}
self.messageHandler.info("Loaded Resolution: " + resNameString)
except Exception as e:
self.messageHandler.error("Cannot load resolutions from " + str(directory) + "\n Info: " + repr(e))
exceptionsCount += 1
if exceptionsCount > 3:
# Will not occur when loading modules with 3 or fewer resolutions, but that should be fine.
self.messageHandler.critical("Failed loading too many resolutions to proceed.")
sys.exit(5)
def getResolutionParameters(self, resolutionCategory, resolutionNameString):
resolutionsList = self.resolutions.get(resolutionCategory)
if resolutionsList is not None and resolutionNameString in resolutionsList:
parameters = self.resolutions[resolutionCategory][resolutionNameString]['parameters']
return parameters
return None
def getResolutionOriginTypes(self, resolutionCategoryNameString: str) -> Union[list, None]:
resolutionCategory, resolutionName = resolutionCategoryNameString.split('/', 1)
try:
if resolutionName in self.resolutions.get(resolutionCategory):
originTypes = self.resolutions[resolutionCategory][resolutionName]['originTypes']
if '*' in originTypes:
originTypes = self.mainWindow.RESOURCEHANDLER.getAllEntities()
return originTypes
except TypeError:
pass
return None
def getResolutionDescription(self, resolutionCategoryNameString: str) -> Union[str, None]:
resolutionCategory, resolutionName = resolutionCategoryNameString.split('/', 1)
try:
if resolutionName in self.resolutions.get(resolutionCategory):
resolutionDescription = self.resolutions[resolutionCategory][resolutionName].get('description', '')
return resolutionDescription
except TypeError:
pass
return None
def loadResolutionsFromServer(self, serverRes) -> None:
for category in serverRes:
if category not in self.resolutions:
self.resolutions[category] = {}
for serverResolution in serverRes[category]:
self.resolutions[category][serverResolution] = serverRes[category][serverResolution]
def removeServerResolutions(self) -> None:
for category in dict(self.resolutions):
for resolution in dict(self.resolutions[category]):
# If resolution class does not exist locally, then assume it exists on the server.
if self.resolutions[category][resolution]['resolution'] == '':
self.resolutions[category].pop(resolution)
for category in dict(self.resolutions):
if len(self.resolutions[category]) == 0:
self.resolutions.pop(category)
def getResolutionCategories(self) -> list:
return list(self.resolutions)
def getResolutionsForEntityTypesByCategory(self, eTypes) -> dict:
"""
Gets a set of entity types, and returns a dictionary with all the resolutions that can take all
included types as input.
If '*' is part of eTypes, get all Resolutions of all categories.
"""
result = {}
for category in self.resolutions:
result[category] = []
for resolution in self.resolutions[category]:
originTypes = self.resolutions[category][resolution]['originTypes']
if eTypes.issubset(originTypes) or '*' in originTypes:
result[category].append(resolution)
return result
def getResolutionsInCategory(self, category) -> list:
if category in self.resolutions:
return list(self.resolutions[category])
return []
def getAllResolutions(self) -> list:
categories = self.getResolutionCategories()
result = []
for category in categories:
result += self.getResolutionsInCategory(category)
return result
def executeResolution(self, resolutionCategoryNameString: str, resolutionEntitiesInput: list, parameters: dict,
resolutionUID: str):
resolutionCategory, resolutionName = resolutionCategoryNameString.split('/', 1)
try:
if resolutionName in self.resolutions.get(resolutionCategory):
if self.resolutions[resolutionCategory][resolutionName].get('resolution') == '':
# If resolution class does not exist locally, then assume it exists on the server.
self.mainWindow.executeRemoteResolution(resolutionCategoryNameString, resolutionEntitiesInput,
parameters, resolutionUID)
# Returning a bool, so we know that the resolution is running on the server.
return True
resolutionClass = self.resolutions[resolutionCategory][resolutionName]['resolution']()
result = resolutionClass.resolution(resolutionEntitiesInput, parameters)
return result
except TypeError:
pass
return None
def createMacro(self, resolutionList: list) -> str:
macroUID = str(uuid4())
self.macros[macroUID] = resolutionList
return macroUID
def deleteMacro(self, macroUID: str) -> bool:
# We don't have to worry about running macros, because the details of the macro are saved in memory.
# We do however want to get the thread lock because of potential race conditions.
try:
with self.mainWindow.macrosLock:
self.macros.pop(macroUID)
return True
except KeyError:
return False
def getMacroFilePath(self):
return Path(self.mainWindow.SETTINGS.value("Project/BaseDir")) / "Project Files" / "Project Macros.lsmacros"
def loadMacros(self):
# Load AFTER we load resolutions.
macroFilePath = self.getMacroFilePath()
try:
with open(macroFilePath, "rb") as macroFile:
self.macros = load(macroFile)
except ValueError:
# If the Macros file is empty or contains invalid input, ignore it.
pass
except FileNotFoundError:
# Create new placeholder notes file if it doesn't exist.
try:
macroFilePath.touch(0o700, exist_ok=False)
self.mainWindow.MESSAGEHANDLER.info('Created new Macros file.')
except FileExistsError:
self.mainWindow.MESSAGEHANDLER.error('Race condition occurred while trying to create Macros file.')
def save(self):
macroFilePath = self.getMacroFilePath()
macroFilePathTmp = macroFilePath.with_suffix(macroFilePath.suffix + '.tmp')
with open(macroFilePathTmp, "wb") as macroFile:
dump(self.macros, macroFile)
move(macroFilePathTmp, macroFilePath)