Source code for jasy.core.Profile

#
# Jasy - Web Tooling Framework
# Copyright 2013-2014 Sebastian Werner
#

import jasy

import time, socket, uuid, getpass, copy
import itertools, json, os

import jasy.core.Console as Console
import jasy.core.FileManager as FileManager
import jasy.core.Util as Util
import jasy.core.Permutation as Permutation
import jasy.core.Locale as Locale
import jasy.core.Project as Project

import jasy.asset.Manager as AssetManager
import jasy.item.Script as ScriptItem

[docs]class Profile(): """ Configuration object for the build profile of the current task """ # # CONFIGURATION DATA STRUCTURE # # Configuration data __data = None # Application fields __fields = None # Application parts __parts = None # Supported commands __commands = None # # CURRENT STATE # # Current permutation __permutation = None # Current permutation bundle __translationBundle = None __timeStamp = None __timeHash = None __fileManager = None def __init__(self, session): Console.info("Initializing profile...") Console.indent() # Reference to global session object self.__session = session # Initialize data instance self.__data = {} # Set default values (which require serialization) self.setJsOutputFolder("js") self.setCssOutputFolder("css") self.setAssetOutputFolder("asset") self.setTemplateOutputFolder("tmpl") self.setCompressionLevel(0) self.setFormattingLevel(100) # Initialize file manager fileManager = self.__fileManager = FileManager.FileManager(self) # Enforce scan of projects session.scan() # Part registry holds information about all parts of the application to build self.__parts = {} # Copy fields and commands from session # This happens to have local access to all of them + being able to add and tweak data locally self.__fields = copy.copy(session.getFields()) self.__commands = copy.copy(session.getCommands()) # Behaves like Date.now() in JavaScript: UTC date in milliseconds self.__timeStamp = int(round(time.time() * 1000)) self.__timeHash = Util.generateChecksum(str(self.__timeStamp)) # No further steps when session has no projects e.g. during "distclean" if not session.getProjects(): return # Initialize asset manager Console.info("Initializing asset manager...") Console.indent() assetManager = self.__assetManager = AssetManager.AssetManager(self) # Registering assets for project in self.getProjects(): assetManager.addProject(project) # Enable sprite sheets and image animations assetManager.processSprites() assetManager.processAnimations() Console.outdent() # Registering commands Console.info("Registering commands...") self.addCommand("asset.url", lambda fileId: assetManager.getAssetUrl(fileId), "url") self.addCommand("asset.width", lambda fileId: assetManager.getAssetWidth(fileId), "px") self.addCommand("asset.height", lambda fileId: assetManager.getAssetHeight(fileId), "px") self.addCommand("sprite.url", lambda fileId: assetManager.getSpriteUrl(fileId), "url") self.addCommand("sprite.left", lambda fileId: assetManager.getSpriteLeft(fileId), "px") self.addCommand("sprite.top", lambda fileId: assetManager.getSpriteTop(fileId), "px") self.addCommand("sprite.width", lambda fileId: assetManager.getSpriteWidth(fileId), "px") self.addCommand("sprite.height", lambda fileId: assetManager.getSpriteHeight(fileId), "px") self.addCommand("animation.columns", lambda fileId: assetManager.getAnimationColumns(fileId), "number") self.addCommand("animation.rows", lambda fileId: assetManager.getAnimationRows(fileId), "number") self.addCommand("animation.frames", lambda fileId: assetManager.getAnimationFrames(fileId), "number") Console.outdent() # # PROJECT API #
[docs] def getProjects(self): """ Returns all currently registered projects. Injects locale project when current permutation has configured a locale. """ projects = self.__session.getProjects() localeProject = self.getCurrentLocaleProject() if localeProject: return projects + [localeProject] return projects # # OBJECT ACCESSORS #
[docs] def getSession(self): return self.__session
[docs] def getAssetManager(self): return self.__assetManager
[docs] def getFileManager(self): return self.__fileManager # # PART MANAGEMENT #
[docs] def getParts(self): return self.__parts
[docs] def registerPart(self, name, className="", styleName="", templateName=""): if name in self.__parts: raise Exception("The part %s is already registered!") self.__parts[name] = { "class" : className, "style" : styleName, "template" : templateName } # # GENERIC STORAGE API #
[docs] def setValue(self, key, value): if value is None: del self.__data[key] else: self.__data[key] = value # Invalidate ID self.__id = None return value
[docs] def getValue(self, key, fallback=None): value = None if key in self.__data: value = self.__data[key] if value is None and fallback is not None: if callable(fallback): value = fallback() else: value = fallback return value
[docs] def getMatchingValues(self, matcher, transformer=None): result = {} data = self.__data for key in data: if matcher(key): if transformer: result[transformer(key)] = data[key] else: result[key] = data[key] return result
[docs] def setFlag(self, name, value): return self.setValue("enable-%s" % name, value)
[docs] def getFlag(self, name, fallback=None): return self.getValue("enable-%s" % name, fallback)
[docs] def setOutputFolder(self, type, value): return self.setValue("output-folder-%s" % type, value)
[docs] def getOutputFolder(self, type, fallback=None): return self.getValue("output-folder-%s" % type, fallback)
[docs] def getOutputFolders(self): return self.getMatchingValues( lambda key: key.startswith("output-folder-"), lambda key: key[14:] ) # # DESTINATION SETUP #
[docs] def getDestinationPath(self): """ Relative or path of the destination folder """ return self.getValue("destination-path", self.__session.getCurrentTask())
[docs] def setDestinationPath(self, path): self.setValue("destination-path", path)
[docs] def getDestinationUrl(self): """ The same as destination folder but from the URL/server perspective """ return self.getValue("destination-url")
[docs] def setDestinationUrl(self, url): # Fix missing end slash if not url.endswith("/"): url += "/" self.setValue("destination-url", url) # # OUTPUT FOLDER NAMES #
[docs] def getCssOutputFolder(self): """ Name of the folder inside the destination folder for storing generated style sheets """ return self.getOutputFolder("css", "css")
[docs] def setCssOutputFolder(self, folder): return self.setOutputFolder("css", folder)
[docs] def getJsOutputFolder(self): """ Name of the folder inside the destination folder for storing generated script files """ return self.getOutputFolder("js", "js")
[docs] def setJsOutputFolder(self, folder): return self.setOutputFolder("js", folder)
[docs] def getAssetOutputFolder(self): """ Name of the folder inside the destination folder for storing used assets """ return self.getOutputFolder("asset", "asset")
[docs] def setAssetOutputFolder(self, folder): return self.setOutputFolder("asset", folder)
[docs] def getTemplateOutputFolder(self): """ Name of the folder inside the destination folder for storing compiled templates """ return self.getOutputFolder("template", "tmpl")
[docs] def setTemplateOutputFolder(self, folder): return self.setOutputFolder("template", folder) # # RUNTIME STATE #
[docs] def getWorkingPath(self): return self.getValue("working-path")
[docs] def setWorkingPath(self, path): return self.setValue("working-path", path)
[docs] def getCurrentTranslation(self): return self.__translationBundle
[docs] def getCurrentPermutation(self): """Returns current permutation object (useful during looping through permutations via permutate()).""" return self.__permutation
[docs] def resetCurrentPermutation(self): """Resets the current permutation object.""" self.__permutation = None # # CONFIGURATION OPTIONS #
[docs] def getHashAssets(self): return self.getFlag("hash-assets")
[docs] def setHashAssets(self, enable): return self.setFlag("hash-assets", enable)
[docs] def getCopyAssets(self): return self.getFlag("copy-assets")
[docs] def setCopyAssets(self, enable): return self.setFlag("copy-assets", enable)
[docs] def getUseSource(self): return self.getFlag("use-source")
[docs] def setUseSource(self, enable): return self.setFlag("use-source", enable) # # OUTPUT FORMATTING/COMPRESSION SETTINGS #
[docs] def getCompressionLevel(self): return self.getValue("compression-level")
[docs] def setCompressionLevel(self, level): return self.setValue("compression-level", level)
[docs] def getFormattingLevel(self): return self.getValue("formatting-level")
[docs] def setFormattingLevel(self, level): return self.setValue("formatting-level", level) # # EXPORT DATA FOR CLIENT SIDE #
[docs] def exportData(self): return { "root" : self.getDestinationUrl() or self.getDestinationFolder() } # # MAIN BUILD METHOD #
def __getEnvironmentId(self): """ Returns a build ID based on environment variables and state """ hostName = socket.gethostname() hostId = uuid.getnode() userName = getpass.getuser() return "host:%s|id:%s|user:%s" % (hostName, hostId, userName)
[docs] def getSetupScripts(self): """ Returns a list of (virtual) classes which are relevant for initial setup. """ setups = {} # Add user configured fields from session setups.update(self.getFieldSetupScripts()) # Info about actual build fieldSetup = "jasy.Env.addField([%s]);" % ('"jasy.build.env",4,"%s"' % self.__getEnvironmentId()) setups["jasy.build.env"] = self.__session.getVirtualItem("jasy.generated.FieldData", ScriptItem.ScriptItem, fieldSetup, ".js") fieldSetup = "jasy.Env.addField([%s]);" % ('"jasy.build.rev",4,"%s"' % self.__session.getMain().getRevision()) setups["jasy.build.rev"] = self.__session.getVirtualItem("jasy.generated.FieldData", ScriptItem.ScriptItem, fieldSetup, ".js") fieldSetup = "jasy.Env.addField([%s]);" % ('"jasy.build.time",4,%s' % self.__timeStamp) setups["jasy.build.time"] = self.__session.getVirtualItem("jasy.generated.FieldData", ScriptItem.ScriptItem, fieldSetup, ".js") # Version of Jasy which was used for build fieldSetup = "jasy.Env.addField([%s]);" % ('"jasy.version",4,"%s"' % jasy.__version__) setups["jasy.version"] = self.__session.getVirtualItem("jasy.generated.FieldData", ScriptItem.ScriptItem, fieldSetup, ".js") # Destination URL e.g. CDN fieldSetup = "jasy.Env.addField([%s]);" % ('"jasy.url",4,"%s"' % (self.getDestinationUrl() or "")) setups["jasy.url"] = self.__session.getVirtualItem("jasy.generated.FieldData", ScriptItem.ScriptItem, fieldSetup, ".js") # Output folder names for key, value in self.getOutputFolders().items(): fieldSetup = "jasy.Env.addField([%s]);" % ('"jasy.folder.%s",4,"%s"' % (key, value)) setups["jasy.folder.%s" % key] = self.__session.getVirtualItem("jasy.generated.OutputFolder", ScriptItem.ScriptItem, fieldSetup, ".js") return setups # # Support for fields # Fields allow to inject data from the build into the running application #
[docs] def setLocales(self, locales, default=None): """ Store locales as a special built-in field with optional default value """ self.__fields["locale"] = { "values" : locales, "default" : default or locales[0], "detect" : "core.detect.Locale" }
[docs] def setDefaultLocale(self, locale): """ Sets the default locale """ if not "locale" in self.__fields: raise UserError("Define locales first!") self.__fields["locale"]["default"] = locale
[docs] def getCurrentLocale(self): """Returns the current locale as defined in current permutation""" permutation = self.getCurrentPermutation() if permutation: locale = permutation.get("locale") if locale: return locale return None
[docs] def getCurrentLocaleProject(self, update=False): """ Returns a locale project for the currently configured locale. Returns None if locale is not set to a valid value. """ locale = self.getCurrentLocale() if not locale: return None path = os.path.abspath(os.path.join(".jasy", "locale", locale)) if not os.path.exists(path) or update: Locale.LocaleParser(locale).export(path) return Project.getProjectFromPath(path, self.__session) # # FIELD API #
[docs] def setField(self, name, value): """ Statically configure the value of the given field. This field is just injected into Permutation data and used for permutations, but as it only holds a single value all alternatives paths are removed/ignored. """ if not name in self.__fields: raise Exception("Unsupported field (not defined by any project): %s" % name) entry = self.__fields[name] # Replace current value with single value entry["values"] = [value] # Additonally set the default entry["default"] = value # Delete detection if configured by the project if "detect" in entry: del entry["detect"]
[docs] def permutateField(self, name, values=None, detect=None, default=None): """ Adds the given key/value pair to the session for permutation usage. It supports an optional test. A test is required as soon as there is more than one value available. The detection method and values are typically already defined by the project declaring the key/value pair. """ if not name in self.__fields: raise Exception("Unsupported field (not defined by any project): %s" % name) entry = self.__fields[name] if values: if type(values) != list: values = [values] entry["values"] = values # Verifying values from build script with value definition in project manifests if "check" in entry: check = entry["check"] for value in values: if check == "Boolean": if type(value) == bool: continue elif check == "String": if type(value) == str: continue elif check == "Number": if type(value) in (int, float): continue else: if value in check: continue raise Exception("Unsupported value %s for %s" % (value, name)) if default is not None: entry["default"] = default elif "check" in entry and entry["check"] == "Boolean": entry["values"] = [True, False] elif "check" in entry and type(entry["check"]) == list: entry["values"] = entry["check"] elif "default" in entry: entry["values"] = [entry["default"]] else: raise Exception("Could not permutate field: %s! Requires value list for non-boolean fields which have no defaults." % name) # Store class which is responsible for detection (overrides data from project) if detect: if not self.getScriptByName(detect): raise Exception("Could not permutate field: %s! Unknown detect class %s." % detect) entry["detect"] = detect
def __exportFieldDetects(self): """ Returns a dict where the field points to the detection class which is being used to figure out the value on the client. """ detects = {} for key in sorted(self.__fields): source = self.__fields[key] if "values" in source: values = source["values"] if "detect" in source and len(values) > 1: detects[key] = source["detect"] else: detects[key] = None # Has no relevance for permutation, just insert the test else: if "detect" in source: detects[key] = source["detect"] elif "default" in source: detects[key] = None return detects
[docs] def getFieldSetupScripts(self): detects = self.__exportFieldDetects() setups = {} for fieldName in detects: fieldSetup = "jasy.Env.addField(%s);" % self.__exportField(fieldName) fieldSetupScript = self.__session.getVirtualItem("jasy.generated.FieldData", jasy.item.Script.ScriptItem, fieldSetup, ".js") setups[fieldName] = fieldSetupScript return setups
def __exportField(self, field): """ Converts data for the given field into a compact data structure for being used to compute a checksum in JavaScript. Export structures: 1. [ name, 1, test, [value1, ...] ] 2. [ name, 2, value ] 3. [ name, 3, test, default? ] 4. [ name, 4, value ] (just data - non permutated - generated internally only) """ source = self.__fields[field] content = [] content.append("'%s'" % field) # We have available values to permutate for if "values" in source: values = source["values"] if "detect" in source and len(values) > 1: # EXPORT STRUCT 1 content.append("1") content.append(source["detect"]) if "default" in source: # Make sure that default value is first in values = values[:] values.remove(source["default"]) values.insert(0, source["default"]) content.append(json.dumps(values)) else: # EXPORT STRUCT 2 content.append("2") if "default" in source: content.append(json.dumps(source["default"])) else: content.append(json.dumps(values[0])) # Has no relevance for permutation, just insert the test else: if "detect" in source: # EXPORT STRUCT 3 content.append("3") # Add detection class content.append(source["detect"]) # Add default value if available if "default" in source: content.append(json.dumps(source["default"])) elif "default" in source: # EXPORT STRUCT 2 content.append("2") content.append(json.dumps(source["default"])) else: # Has no detection and no permutation. Ignore it completely pass return "[%s]" % ", ".join(content) # # State Handling / Looping # def __generatePermutations(self): """ Combines all values to a set of permutations. These define all possible combinations of the configured settings """ fields = self.__fields values = {} for key in fields: if "values" in fields[key]: values[key] = fields[key]["values"] elif "default" in fields[key] and not "detect" in fields[key]: values[key] = [fields[key]["default"]] # Thanks to eumiro via http://stackoverflow.com/questions/3873654/combinations-from-dictionary-with-list-values-using-python names = sorted(values) combinations = [dict(zip(names, prod)) for prod in itertools.product(*(values[name] for name in names))] permutations = [Permutation.getPermutation(combi) for combi in combinations] return permutations
[docs] def permutate(self): """ Generator method for permutations for improving output capabilities """ Console.info("Processing permutations...") Console.indent() permutations = self.__generatePermutations() length = len(permutations) for pos, current in enumerate(permutations): Console.info("Permutation %s/%s:" % (pos+1, length)) Console.indent() self.__permutation = current self.__translationBundle = self.__session.getTranslationBundle(self.getCurrentLocale()) yield current Console.outdent() Console.outdent() self.__permutation = None self.__translationBundle = None
[docs] def getCurrentOptimization(self): return None
[docs] def getCurrentFormatting(self): return None
[docs] def setStaticPermutation(self, **argv): """ Sets current permutation to a static permutation which contains all values hardly wired to static values using setField() or given via additional named parameters. """ combi = {} for name in self.__fields: entry = self.__fields[name] if not "detect" in entry: combi[name] = entry["default"] for name in argv: combi[name] = argv[name] if not combi: self.__permutation = None return None permutation = Permutation.getPermutation(combi) self.__permutation = permutation return permutation
[docs] def expandFileName(self, fileName): """ Replaces placeholders inside the given filename and returns the result. The placeholders are based on the current state of the session. These are the currently supported placeholders: - {{locale}}: Name of current locale e.g. de_DE - {{permutation}}: SHA1 checksum of current permutation - {{id}}: SHA1 checksum based on permutation and repository branch/revision """ if "{{destination}}" in fileName: fileName = fileName.replace("{{destination}}", self.getDestinationPath()) if self.__permutation: if "{{permutation}}" in fileName: fileName = fileName.replace("{{permutation}}", self.__permutation.getChecksum()) if "{{id}}" in fileName: buildId = "%s@%s" % (self.__permutation.getKey(), self.__session.getMain().getRevision()) buildHash = Util.generateChecksum(buildId) fileName = fileName.replace("{{id}}", buildHash) if "{{locale}}" in fileName: locale = self.__permutation.get("locale") fileName = fileName.replace("{{locale}}", locale) elif "{{id}}" in fileName: fileName = fileName.replace("{{id}}", "none@%s" % (self.getMain().getRevision())) return fileName
[docs] def addCommand(self, name, func, resultType=None, globalName=False): """ Registers the given function as a new command """ if globalName and "." in name: raise Exception("Invalid global name: %s!" % name) elif not globalName and len(name.split(".")) != 2: raise Exception("Command names should always match namespace.name! Tried with: %s!" % name) commands = self.__commands if name in commands: raise Exception("Overwriting commands is not supported! Command=%s" % name) commands[name] = { "func" : func, "type" : resultType }
[docs] def executeCommand(self, command, params=None): """ Executes the given command and returns the result """ commands = self.__commands # Delegate unknown commands to the Session instance if not command in commands: return self.__session.executeCommand(command, params) raise UserError("Unsupported command %s" % command) entry = commands[command] resultType = entry["type"] if params: result = entry["func"](*params) else: result = entry["func"]() return result, resultType
[docs] def getId(self): id = self.__id if id is None: all = { "parts" : self.__parts, "permutation" : str(self.__permutation), "data" : self.__data } serialized = json.dumps(all, sort_keys=True, indent=2, separators=(',', ': ')) # Console.info("Re-generating ID: %s" % serialized) id = self.__id = jasy.core.Util.generateChecksum(serialized) Console.debug("Re-generated profile ID: %s" % id) return id