diff --git a/avalon/io.py b/avalon/io.py
index 5243660c9..ff918ac12 100644
--- a/avalon/io.py
+++ b/avalon/io.py
@@ -114,6 +114,9 @@ def _from_environment():
session = {
item[0]: os.getenv(item[0], item[1])
for item in (
+ # The schema name that should be used to validate this session
+ ("AVALON_SESSION_SCHEMA", "avalon-core:session-2.0"),
+
# Root directory of projects on disk
("AVALON_PROJECTS", None),
@@ -132,6 +135,9 @@ def _from_environment():
# Name of current app
("AVALON_APP", None),
+ # Full name of current app (e.g. versioned name)
+ ("AVALON_APP_NAME", None),
+
# Path to working directory
("AVALON_WORKDIR", None),
@@ -199,7 +205,7 @@ def _from_environment():
) if os.getenv(item[0], item[1]) is not None
}
- session["schema"] = "avalon-core:session-2.0"
+ session["schema"] = session["AVALON_SESSION_SCHEMA"]
try:
schema.validate(session)
except schema.ValidationError as e:
diff --git a/avalon/maya/commands.py b/avalon/maya/commands.py
index c2eab8b51..8987edc0a 100644
--- a/avalon/maya/commands.py
+++ b/avalon/maya/commands.py
@@ -14,8 +14,15 @@
def reset_frame_range():
"""Set frame range to current asset"""
- shot = api.Session["AVALON_ASSET"]
- shot = io.find_one({"name": shot, "type": "asset"})
+ shot_name = api.Session.get("AVALON_ASSET")
+ if shot_name is None:
+ cmds.warning("No AVALON_ASSET setup in current working session.")
+ return
+
+ shot = io.find_one({"name": shot_name, "type": "asset"})
+ if shot is None:
+ cmds.error("Shot '%s' not found in database." % shot_name)
+ return
try:
diff --git a/avalon/maya/pipeline.py b/avalon/maya/pipeline.py
index ba4005b99..73e1cfa71 100644
--- a/avalon/maya/pipeline.py
+++ b/avalon/maya/pipeline.py
@@ -125,8 +125,8 @@ def deferred():
# Create context menu
context_label = "{}, {}".format(
- api.Session["AVALON_ASSET"],
- api.Session["AVALON_TASK"]
+ api.Session.get("AVALON_ASSET", "--"),
+ api.Session.get("AVALON_TASK", "--")
)
cmds.menuItem(
@@ -275,8 +275,8 @@ def _update_menu_task_label():
logger.warning("Can't find menuItem: {}".format(object_name))
return
- label = "{}, {}".format(api.Session["AVALON_ASSET"],
- api.Session["AVALON_TASK"])
+ label = "{}, {}".format(api.Session.get("AVALON_ASSET", "--"),
+ api.Session.get("AVALON_TASK", "--"))
cmds.menuItem(object_name, edit=True, label=label)
diff --git a/avalon/nuke/command.py b/avalon/nuke/command.py
index 58106995a..2a571723d 100644
--- a/avalon/nuke/command.py
+++ b/avalon/nuke/command.py
@@ -18,8 +18,16 @@ def reset_frame_range():
fps = float(api.Session.get("AVALON_FPS", 25))
nuke.root()["fps"].setValue(fps)
- name = api.Session["AVALON_ASSET"]
+ name = api.Session.get("AVALON_ASSET")
+ if name is None:
+ log.warning("No AVALON_ASSET setup in current working session.")
+ return
+
asset = io.find_one({"name": name, "type": "asset"})
+ if asset is None:
+ log.error("No AVALON_ASSET setup in current working session.")
+ return
+
asset_data = asset["data"]
handles = get_handles(asset)
diff --git a/avalon/nuke/pipeline.py b/avalon/nuke/pipeline.py
index 149675c15..35cd7bbb0 100644
--- a/avalon/nuke/pipeline.py
+++ b/avalon/nuke/pipeline.py
@@ -274,7 +274,8 @@ def _install_menu():
menu = menubar.addMenu(api.Session["AVALON_LABEL"])
label = "{0}, {1}".format(
- api.Session["AVALON_ASSET"], api.Session["AVALON_TASK"]
+ api.Session.get("AVALON_ASSET", "--"),
+ api.Session.get("AVALON_TASK", "--")
)
context_action = menu.addCommand(label)
context_action.setEnabled(False)
diff --git a/avalon/pipeline.py b/avalon/pipeline.py
index c1d9f1f18..e79292176 100644
--- a/avalon/pipeline.py
+++ b/avalon/pipeline.py
@@ -59,7 +59,7 @@ def install(host):
io.install()
missing = list()
- for key in ("AVALON_PROJECT", "AVALON_ASSET"):
+ for key in ("AVALON_PROJECT", ):
if key not in Session:
missing.append(key)
@@ -72,6 +72,11 @@ def install(host):
project = Session["AVALON_PROJECT"]
log.info("Activating %s.." % project)
+ if not os.getenv("_AVALON_APP_INITIALIZED"):
+ log.info("Initializing working directory..")
+ no_predefined_workdir = "AVALON_WORKDIR" not in Session
+ initialize(Session, reset_workdir=no_predefined_workdir)
+
config = find_config()
# Optional host install function
@@ -94,6 +99,35 @@ def install(host):
log.info("Successfully installed Avalon!")
+def initialize(session, reset_workdir):
+ """Initialize Work Directory
+
+ This finds the current AVALON_APP_NAME and tries to triggers its
+ `.toml` initialization step. Note that this will only be valid
+ whenever `AVALON_APP_NAME` is actually set in the current session.
+
+ """
+ # Find the application definition
+ app_name = session.get("AVALON_APP_NAME")
+ if not app_name:
+ log.error("No AVALON_APP_NAME session variable is set. "
+ "Unable to initialize app Work Directory.")
+ return
+
+ app_definition = lib.get_application(app_name)
+ App = type(
+ "app_%s" % app_name,
+ (Application,),
+ {
+ "name": app_name,
+ "config": app_definition.copy()
+ }
+ )
+ app = App()
+ env = app.environ(session, reset_workdir)
+ app.initialize(env)
+
+
def find_config():
log.info("Finding configuration for project..")
@@ -344,7 +378,7 @@ def is_compatible(self, session):
return False
return True
- def environ(self, session):
+ def environ(self, session, reset_workdir=True):
"""Build application environment"""
session = session.copy()
@@ -352,10 +386,11 @@ def environ(self, session):
session["AVALON_APP_NAME"] = self.name
# Compute work directory
- project = io.find_one({"type": "project"})
- template = project["config"]["template"]["work"]
- workdir = _format_work_template(template, session)
- session["AVALON_WORKDIR"] = os.path.normpath(workdir)
+ if reset_workdir:
+ project = io.find_one({"type": "project"})
+ template = project["config"]["template"]["work"]
+ workdir = _format_work_template(template, session)
+ session["AVALON_WORKDIR"] = os.path.normpath(workdir)
# Construct application environment from .toml config
app_environment = self.config.get("environment", {})
@@ -391,24 +426,24 @@ def initialize(self, environment):
workdir = environment["AVALON_WORKDIR"]
workdir_existed = os.path.exists(workdir)
if not workdir_existed:
- os.makedirs(workdir)
self.log.info("Creating working directory '%s'" % workdir)
+ os.makedirs(workdir)
- # Create default directories from app configuration
- default_dirs = self.config.get("default_dirs", [])
- default_dirs = self._format(default_dirs, **environment)
- if default_dirs:
- self.log.debug("Creating default directories..")
- for dirname in default_dirs:
- try:
- os.makedirs(os.path.join(workdir, dirname))
- self.log.debug(" - %s" % dirname)
- except OSError as e:
- # An already existing default directory is fine.
- if e.errno == errno.EEXIST:
- pass
- else:
- raise
+ # Create default directories from app configuration
+ default_dirs = self.config.get("default_dirs", [])
+ default_dirs = self._format(default_dirs, **environment)
+ if default_dirs:
+ self.log.debug("Creating default directories..")
+ for dirname in default_dirs:
+ try:
+ os.makedirs(os.path.join(workdir, dirname))
+ self.log.debug(" - %s" % dirname)
+ except OSError as e:
+ # An already existing default directory is fine.
+ if e.errno == errno.EEXIST:
+ pass
+ else:
+ raise
# Perform application copy
for src, dst in self.config.get("copy", {}).items():
@@ -416,6 +451,9 @@ def initialize(self, environment):
# Expand env vars
src, dst = self._format([src, dst], **environment)
+ if os.path.isfile(dst):
+ continue
+
try:
self.log.info("Copying %s -> %s" % (src, dst))
shutil.copy(src, dst)
@@ -423,7 +461,7 @@ def initialize(self, environment):
self.log.error("Could not copy application file: %s" % e)
self.log.error(" - %s -> %s" % (src, dst))
- def launch(self, environment):
+ def launch(self, environment, initialized=False):
executable = lib.which(self.config["executable"])
if executable is None:
@@ -432,6 +470,9 @@ def launch(self, environment):
% (self.config["executable"], os.getenv("PATH"))
)
+ if initialized:
+ environment["_AVALON_APP_INITIALIZED"] = "1"
+
args = self.config.get("args", [])
return lib.launch(
executable=executable,
@@ -444,12 +485,14 @@ def process(self, session, **kwargs):
"""Process the full Application action"""
environment = self.environ(session)
+ initialized = False
if kwargs.get("initialize", True):
self.initialize(environment)
+ initialized = True
if kwargs.get("launch", True):
- return self.launch(environment)
+ return self.launch(environment, initialized)
def _format(self, original, **kwargs):
"""Utility recursive dict formatting that logs the error clearly."""
diff --git a/avalon/schema/session-3.0.json b/avalon/schema/session-3.0.json
new file mode 100644
index 000000000..4513e6692
--- /dev/null
+++ b/avalon/schema/session-3.0.json
@@ -0,0 +1,146 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+
+ "title": "avalon-core:session-3.0",
+ "description": "The Avalon environment",
+
+ "type": "object",
+
+ "additionalProperties": true,
+
+ "required": [
+ "AVALON_PROJECTS",
+ "AVALON_PROJECT",
+ "AVALON_CONFIG"
+ ],
+
+ "properties": {
+ "AVALON_PROJECTS": {
+ "description": "Absolute path to root of project directories",
+ "type": "string",
+ "example": "/nas/projects"
+ },
+ "AVALON_PROJECT": {
+ "description": "Name of project",
+ "type": "string",
+ "pattern": "^\\w*$",
+ "example": "Hulk"
+ },
+ "AVALON_ASSET": {
+ "description": "Name of asset",
+ "type": "string",
+ "pattern": "^\\w*$",
+ "example": "Bruce"
+ },
+ "AVALON_SILO": {
+ "description": "Name of asset group or container",
+ "type": "string",
+ "pattern": "^\\w*$",
+ "example": "assets"
+ },
+ "AVALON_TASK": {
+ "description": "Name of task",
+ "type": "string",
+ "pattern": "^\\w*$",
+ "example": "modeling"
+ },
+ "AVALON_CONFIG": {
+ "description": "Name of Avalon configuration",
+ "type": "string",
+ "pattern": "^\\w*$",
+ "example": "polly"
+ },
+ "AVALON_WORKDIR": {
+ "description": "Current working directory of a host, such as Maya's location of workspace.mel",
+ "type": "string",
+ "example": "/mnt/projects/alita/assets/vector/maya"
+ },
+ "AVALON_APP": {
+ "description": "Name of application",
+ "type": "string",
+ "pattern": "^\\w*$",
+ "example": "maya2016"
+ },
+ "AVALON_MONGO": {
+ "description": "Address to the asset database",
+ "type": "string",
+ "pattern": "^mongodb://[\\w/@:.]*$",
+ "example": "mongodb://localhost:27017",
+ "default": "mongodb://localhost:27017"
+ },
+ "AVALON_DB": {
+ "description": "Name of database",
+ "type": "string",
+ "pattern": "^\\w*$",
+ "example": "avalon",
+ "default": "avalon"
+ },
+ "AVALON_LABEL": {
+ "description": "Nice name of Avalon, used in e.g. graphical user interfaces",
+ "type": "string",
+ "example": "Mindbender",
+ "default": "Avalon"
+ },
+ "AVALON_SENTRY": {
+ "description": "Address to Sentry",
+ "type": "string",
+ "pattern": "^http[\\w/@:.]*$",
+ "example": "https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2",
+ "default": null
+ },
+ "AVALON_DEADLINE": {
+ "description": "Address to Deadline",
+ "type": "string",
+ "pattern": "^http[\\w/@:.]*$",
+ "example": "http://192.168.99.101",
+ "default": null
+ },
+ "AVALON_TIMEOUT": {
+ "description": "Wherever there is a need for a timeout, this is the default value.",
+ "type": "string",
+ "pattern": "^[0-9]*$",
+ "default": "1000",
+ "example": "1000"
+ },
+ "AVALON_UPLOAD": {
+ "description": "Boolean of whether to upload published material to central asset repository",
+ "type": "string",
+ "default": null,
+ "example": "True"
+ },
+ "AVALON_USERNAME": {
+ "description": "Generic username",
+ "type": "string",
+ "pattern": "^\\w*$",
+ "default": "avalon",
+ "example": "myself"
+ },
+ "AVALON_PASSWORD": {
+ "description": "Generic password",
+ "type": "string",
+ "pattern": "^\\w*$",
+ "default": "secret",
+ "example": "abc123"
+ },
+ "AVALON_INSTANCE_ID": {
+ "description": "Unique identifier for instances in a working file",
+ "type": "string",
+ "pattern": "^[\\w.]*$",
+ "default": "avalon.instance",
+ "example": "avalon.instance"
+ },
+ "AVALON_CONTAINER_ID": {
+ "description": "Unique identifier for a loaded representation in a working file",
+ "type": "string",
+ "pattern": "^[\\w.]*$",
+ "default": "avalon.container",
+ "example": "avalon.container"
+ },
+ "AVALON_DEBUG": {
+ "description": "Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.",
+ "type": "string",
+ "default": null,
+ "example": "True"
+ }
+ }
+}
diff --git a/avalon/tools/creator/app.py b/avalon/tools/creator/app.py
index 25c79c1d6..ea2e0a00d 100644
--- a/avalon/tools/creator/app.py
+++ b/avalon/tools/creator/app.py
@@ -396,7 +396,7 @@ def refresh(self):
listing = self.data["Listing"]
asset = self.data["Asset"]
- asset.setText(api.Session["AVALON_ASSET"])
+ asset.setText(api.Session.get("AVALON_ASSET", ""))
has_families = False
diff --git a/avalon/tools/loader/app.py b/avalon/tools/loader/app.py
index a0fd8337a..6b418b86a 100644
--- a/avalon/tools/loader/app.py
+++ b/avalon/tools/loader/app.py
@@ -430,7 +430,7 @@ def show(debug=False, parent=None, use_context=False):
window.setStyleSheet(style.load_stylesheet())
if use_context:
- context = {"asset": api.Session["AVALON_ASSET"]}
+ context = {"asset": api.Session.get("AVALON_ASSET")}
window.set_context(context, refresh=True)
else:
window.refresh()
diff --git a/avalon/tools/workfiles/app.py b/avalon/tools/workfiles/app.py
index 70c614de7..a0d3a7841 100644
--- a/avalon/tools/workfiles/app.py
+++ b/avalon/tools/workfiles/app.py
@@ -651,23 +651,8 @@ def initialize_work_directory(self):
asset=self._asset,
task=self._task)
session.update(changes)
-
- # Find the application definition
- app_name = os.environ.get("AVALON_APP_NAME")
- if not app_name:
- log.error("No AVALON_APP_NAME session variable is set. "
- "Unable to initialize app Work Directory.")
- return
-
- app_definition = pipeline.lib.get_application(app_name)
- App = type("app_%s" % app_name,
- (pipeline.Application,),
- {"config": app_definition.copy()})
-
# Initialize within the new session's environment
- app = App()
- env = app.environ(session)
- app.initialize(env)
+ pipeline.initialize(session, reset_workdir=True)
# Force a full to the asset as opposed to just self.refresh() so
# that it will actually check again whether the Work directory exists
@@ -805,7 +790,7 @@ def on_asset_changed(self):
def set_context(self, context):
- if "asset" in context:
+ if context.get("asset"):
asset = context["asset"]
asset_document = io.find_one({
"name": asset,
@@ -818,7 +803,7 @@ def set_context(self, context):
# Force a refresh on Tasks?
self.widgets["tasks"].set_asset(asset_document)
- if "task" in context:
+ if context.get("task"):
self.widgets["tasks"].select_task(context["task"])
def refresh(self):
@@ -892,9 +877,8 @@ def show(root=None, debug=False, parent=None, use_context=True):
window.refresh()
if use_context:
- context = {"asset": api.Session["AVALON_ASSET"],
- "silo": api.Session["AVALON_SILO"],
- "task": api.Session["AVALON_TASK"]}
+ context = {"asset": api.Session.get("AVALON_ASSET"),
+ "task": api.Session.get("AVALON_TASK")}
window.set_context(context)
window.show()
diff --git a/res/houdini/MainMenuCommon.XML b/res/houdini/MainMenuCommon.XML
index a492ef7cd..c72ece8a0 100644
--- a/res/houdini/MainMenuCommon.XML
+++ b/res/houdini/MainMenuCommon.XML
@@ -6,7 +6,8 @@