diff --git a/README.md b/README.md
index cce347e..7e7f68a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
### Pyblish Starter
-A basic asset creation pipeline, with batteries included.
+A basic asset creation pipeline - batteries included.
> WARNING: Not ready for use.
@@ -48,6 +48,12 @@ From here, you model, rig and animate as per the contract below.
Build your own asset creation pipeline, starting with the basics.
+**At a glance**
+
+- Categorise nodes within your workfile as being part of a single asset.
+- Enable an asset library to identify published data on disk.
+- Track when, where and from whom each asset come from.
+
**Overview**
Asset creation covers all aspects related to building the assets used in the production of film. A film is typically partitioned into sequences and shots, where each shot consists of one or more assets.
@@ -69,6 +75,15 @@ It includes a series of graphical user interfaces to aid the user in conforming
+### Batteries
+
+In addition to Pyblish cooperative plug-ins, a series of template workflow utilities are included.
+
+...
+
+
+
+
### Terminology
Starter reserves the following words for private and public use. Public members are exposed to the user, private ones are internal to the implementation.
@@ -80,8 +95,35 @@ Starter reserves the following words for private and public use. Public members
| ![][ver] | `version` | `X` | An asset iteration | v1, v034
| ![][rep] | `representation` | | A data format | Maya file, pointcache, thumbnail
| ![][for] | `format` | | A file extension | `.ma`, `.abc`, `.ico`, `.png`
-| ![][for] | `public` | | Shared data | v034 of Ryan
-| ![][for] | `private` | | User data | Scenefile for v034 of Ryan
+| ![][for] | `shared` | `X` | Public data | v034 of Ryan
+| ![][for] | `user` | `X` | Private data | Scenefile for v034 of Ryan
+
+
+
+### Shared/user separation
+
+This project separates between data in progress, and data shared with others.
+
+Data in progress is any data in which a shared data is being produced. It is highly **mutable** and typically **private** to an individual artist.
+
+- **Mutable** implies
+
+Shared data on the other hand is **immutable**, **correct** and **impersonal**.
+
+- **Immutable** implies that the data may be dependent upon by other data.
+- **Correct** implies passing validation of the associated family.
+- **Impersonal** implies following strict organiasational conventions.
+
+### Ids
+
+...
+
+| Name | Description | Example
+|:-----------------------------|:-------------------------|:----------
+| `pyblish.starter.container` | Incoming unit of data | `...:model_GRP`, `...:rig_GRP`
+| `pyblish.starter.instance` | Outgoing unit of data | `Strange_model_default`
+
+
[ver]: https://cloud.githubusercontent.com/assets/2152766/18576835/f6b80574-7bdc-11e6-8237-1227f779815a.png
[ast]: https://cloud.githubusercontent.com/assets/2152766/18576836/f6ca19e4-7bdc-11e6-9ef8-3614474c58bb.png
@@ -140,6 +182,7 @@ A generic representation of geometry.
![req][] **Requirements**
+- All DAG nodes must be parented to a single top-level transform
- Static geometry (no deformers, generators) `*`
- One shape per transform `*`
- Zero transforms and pivots `*`
@@ -177,6 +220,7 @@ The `starter.rig` contains the necessary implementation and interface for animat
![req][] **Requirements**
+- All DAG nodes must be parented to a single top-level transform
- Must contain an `objectSet` for controls and cachable geometry
- Channels in `controls_SEL` at *default* values`*`
- No input connection to animatable channel in `controls_SEL` `*`
diff --git a/patch b/patch
deleted file mode 100644
index 9f89752..0000000
--- a/patch
+++ /dev/null
@@ -1,1171 +0,0 @@
-From dad345e4021b5d0d51ae35904e479d2961e0329d Mon Sep 17 00:00:00 2001
-From: mottosso
-Date: Thu, 22 Sep 2016 16:20:25 +0100
-Subject: [PATCH] Implement basic schema for ls()
-
----
- .noserc | 5 -
- pyblish_starter/__init__.py | 14 ++-
- pyblish_starter/lib.py | 115 ++++++++++
- pyblish_starter/maya/__init__.py | 6 +
- pyblish_starter/maya/lib.py | 129 ++++++++++--
- pyblish_starter/pipeline.py | 292 +++++++++++++++-----------
- pyblish_starter/plugins/collect_instances.py | 5 +-
- pyblish_starter/plugins/extract_animation.py | 1 +
- pyblish_starter/plugins/extract_model.py | 1 +
- pyblish_starter/plugins/extract_rig.py | 1 +
- pyblish_starter/plugins/integrate_asset.py | 37 +++-
- pyblish_starter/tests/test_pipeline.py | 134 +++++++++++-
- pyblish_starter/tools/asset_loader/app.py | 57 +++++-
- run_tests.py | 4 +-
- setup.py | 2 +-
- 15 files changed, 631 insertions(+), 172 deletions(-)
- create mode 100644 pyblish_starter/lib.py
-
-diff --git a/.noserc b/.noserc
-index 0f986f5..c509d78 100644
---- a/.noserc
-+++ b/.noserc
-@@ -1,9 +1,4 @@
- [nosetests]
- verbosity=2
- with-doctest=1
--with-coverage=1
- exclude=vendor
--cover-html=1
--cover-erase=1
--cover-tests=1
--cover-package=pyblish_starter
-\ No newline at end of file
-diff --git a/pyblish_starter/__init__.py b/pyblish_starter/__init__.py
-index 12f37cb..b4cf81e 100644
---- a/pyblish_starter/__init__.py
-+++ b/pyblish_starter/__init__.py
-@@ -1,16 +1,28 @@
-+"""Public API
-+
-+Anything that isn't defined here is INTERNAL and unreliable for external use.
-+
-+"""
-+
- from .pipeline import (
- ls,
- install,
-+ uninstall,
- register_host,
- register_plugins,
-- format_private_dir,
- )
-
-+from .lib import (
-+ format_private_dir,
-+ format_version,
-+)
-
- __all__ = [
- "ls",
- "install",
-+ "uninstall",
- "register_host",
- "register_plugins",
- "format_private_dir",
-+ "format_version",
- ]
-diff --git a/pyblish_starter/lib.py b/pyblish_starter/lib.py
-new file mode 100644
-index 0000000..31fba01
---- /dev/null
-+++ b/pyblish_starter/lib.py
-@@ -0,0 +1,115 @@
-+import os
-+import re
-+import datetime
-+
-+
-+def time():
-+ return datetime.datetime.now().strftime("%Y-%m-%dT%H-%M-%SZ")
-+
-+
-+def format_private_dir(root, name):
-+ dirname = os.path.join(root, "private", time(), name)
-+ return dirname
-+
-+
-+def parse_version(version):
-+ """Return integer version from formatted string
-+
-+ Example:
-+ >>> parse_version("v001")
-+ 1
-+ >>> parse_version("2")
-+ 2
-+ >>> parse_version("version03")
-+ 3
-+ >>> parse_version("000008")
-+ 8
-+ >>> parse_version("abc")
-+ Traceback (most recent call last):
-+ ...
-+ ValueError: Could not parse "abc"
-+
-+ """
-+
-+ matches = re.findall(r"\d+", version)
-+
-+ if not matches:
-+ raise ValueError("Could not parse \"%s\"" % version)
-+
-+ return int(matches[-1])
-+
-+
-+def format_version(version):
-+ """Produce filesystem-friendly string from integer version
-+
-+ Arguments:
-+ version (int): Version number
-+
-+ Returns:
-+ string of `version`.
-+
-+ Raises:
-+ TypeError on non-integer version
-+
-+ Example:
-+ >>> format_version(5)
-+ 'v005'
-+ >>> format_version("x")
-+ Traceback (most recent call last):
-+ ...
-+ TypeError: %d format: a number is required, not str
-+
-+ """
-+
-+ return "v%03d" % version
-+
-+
-+def find_latest_version(versions):
-+ """Return latest version from list of versions
-+
-+ If multiple numbers are found in a single version,
-+ the last one found is used. E.g. (6) from "v7_22_6"
-+
-+ Arguments:
-+ versions (list): Version numbers as string
-+
-+ Example:
-+ >>> find_next_version(["v001", "v002", "v003"])
-+ 4
-+ >>> find_next_version(["1", "2", "3"])
-+ 4
-+ >>> find_next_version(["v1", "v0002", "verision_3"])
-+ 4
-+ >>> find_next_version(["v2", "5_version", "verision_8"])
-+ 9
-+ >>> find_next_version(["v2", "v3_5", "_1_2_3", "7, 4"])
-+ 6
-+ >>> find_next_version(["v010", "v011"])
-+ 12
-+
-+ """
-+
-+ highest_version = 0
-+ for version in versions:
-+ version = parse_version(version)
-+
-+ if version > highest_version:
-+ highest_version = version
-+
-+ return highest_version
-+
-+
-+def find_next_version(versions):
-+ """Return next version from list of versions
-+
-+ See docstring for :func:`find_latest_version`.
-+
-+ Arguments:
-+ versions (list): Version numbers as string
-+
-+ Returns:
-+ int: Next version number
-+
-+ """
-+
-+ return find_latest_version(versions) + 1
-diff --git a/pyblish_starter/maya/__init__.py b/pyblish_starter/maya/__init__.py
-index ec672cf..f579201 100644
---- a/pyblish_starter/maya/__init__.py
-+++ b/pyblish_starter/maya/__init__.py
-@@ -1,3 +1,9 @@
-+"""Public API
-+
-+Anything that isn't defined here is INTERNAL and unreliable for external use.
-+
-+"""
-+
- from .lib import (
- install,
- uninstall,
-diff --git a/pyblish_starter/maya/lib.py b/pyblish_starter/maya/lib.py
-index 64ceb68..e8c0411 100644
---- a/pyblish_starter/maya/lib.py
-+++ b/pyblish_starter/maya/lib.py
-@@ -1,10 +1,11 @@
-+import os
- import sys
- import logging
-
- from maya import cmds, mel
-
- from .. import pipeline
--from ..vendor.Qt import QtWidgets
-+from ..vendor.Qt import QtWidgets, QtGui
-
- self = sys.modules[__name__]
- self.log = logging.getLogger()
-@@ -12,11 +13,20 @@ self.menu = "pyblishStarter"
-
-
- def install():
-+ try:
-+ import pyblish_maya
-+ assert pyblish_maya.is_setup()
-+
-+ except (ImportError, AssertionError):
-+ _display_missing_dependencies()
-+
- install_menu()
-+ register_formats()
-
-
- def uninstall():
- uninstall_menu()
-+ deregister_formats()
-
-
- def install_menu():
-@@ -24,14 +34,17 @@ def install_menu():
-
- uninstall_menu()
-
-- cmds.menu(label="Pyblish Starter", tearOff=True, parent="MayaWindow")
-+ cmds.menu(self.menu,
-+ label="Pyblish Starter",
-+ tearOff=True,
-+ parent="MayaWindow")
- cmds.menuItem("Create Instance", command=instance_creator.show)
- cmds.menuItem("Load Asset", command=asset_loader.show)
-
-
- def uninstall_menu():
-- widgets = {w.objectName(): w for w in QtWidgets.qApp.allWidgets()}
-- menu = widgets.get("pyblishStarter")
-+ widgets = dict((w.objectName(), w) for w in QtWidgets.qApp.allWidgets())
-+ menu = widgets.get(self.menu)
-
- if menu:
- menu.deleteLater()
-@@ -44,6 +57,18 @@ def root():
- cmds.workspace(directory=True, query=True))
-
-
-+def register_formats():
-+ pipeline.register_format(".ma")
-+ pipeline.register_format(".mb")
-+ pipeline.register_format(".abc")
-+
-+
-+def deregister_formats():
-+ pipeline.deregister_format(".ma")
-+ pipeline.deregister_format(".mb")
-+ pipeline.deregister_format(".abc")
-+
-+
- def hierarchy_from_string(hierarchy):
- parents = {}
-
-@@ -83,33 +108,59 @@ def outmesh(shape, name=None):
- outmesh = cmds.listRelatives(outmesh, parent=True)[0]
- outmesh = cmds.rename(outmesh, name or "outMesh1")
- cmds.sets(outmesh, addElement="initialShadingGroup")
-+
- return outmesh
-
-
--def loader(asset, version=-1, namespace=None):
-+def loader(asset, version=-1):
- """Load asset
-
-- The loader formats the `pipeline.root` variable with the
-- following template members.
--
-- - {project}: Absolute path to Maya project root.
--
- Arguments:
-- asset (str): Name of asset
-+ asset (dict): Object of schema "pyblish-starter:asset-1.0"
- version (int, optional): Version number, defaults to latest
-- namespace (str, optional): Name of namespace
-+ representation (str, optional): Representation to load,
-+ defaults to "any"
-
- Returns:
- Reference node
-
-+ Raises:
-+ IndexError on version not found
-+ ValueError on no supported representation
-+
- """
-
-+ assert asset["schema"] == "pyblish-starter:asset-1.0"
- assert isinstance(version, int), "Version must be integer"
-
-- fname = pipeline.abspath(asset, version, ".ma").replace("\\", "/")
-+ try:
-+ version = asset["versions"][version]
-+ except IndexError:
-+ raise IndexError("\"%s\" of \"%s\" not found." % (version, asset))
-+
-+ formats = pipeline.registered_formats()
-+
-+ # Pick any representation
-+ representation = next(
-+ (rep for rep in version["representations"]
-+ if rep["format"] in formats), None
-+ )
-+
-+ if representation is None:
-+ raise ValueError(
-+ "No supported representations for %s\n"
-+ "Supported representations: %s" % (
-+ asset["name"],
-+ ", ".join(r["format"] for r in version["representations"]))
-+ )
-+
-+ fname = representation["path"].format(
-+ asset=asset["path"],
-+ version=pipeline.parse_version(version["version"])
-+ )
-
- nodes = cmds.file(fname,
-- namespace=namespace or ":",
-+ namespace=asset["name"] + "_",
- reference=True)
-
- return cmds.referenceQuery(nodes, referenceNode=True)
-@@ -212,3 +263,53 @@ def export_alembic(nodes, file, frame_range=None, uv_write=True):
- mel_cmd = "AbcExport -j \"{0}\"".format(mel_args_string)
-
- return mel.eval(mel_cmd)
-+
-+
-+def _display_missing_dependencies():
-+ import pyblish
-+
-+ messagebox = QtWidgets.QMessageBox()
-+ messagebox.setIcon(messagebox.Warning)
-+ messagebox.setWindowIcon(QtGui.QIcon(os.path.join(
-+ os.path.dirname(pyblish.__file__),
-+ "icons",
-+ "logo-32x32.svg"))
-+ )
-+
-+ spacer = QtWidgets.QWidget()
-+ spacer.setMinimumSize(400, 0)
-+ spacer.setSizePolicy(QtWidgets.QSizePolicy.Minimum,
-+ QtWidgets.QSizePolicy.Expanding)
-+
-+ layout = messagebox.layout()
-+ layout.addWidget(spacer, layout.rowCount(), 0, 1, layout.columnCount())
-+
-+ messagebox.setWindowTitle("Uh oh")
-+ messagebox.setText("Missing dependencies")
-+
-+ messagebox.setInformativeText("""\
-+pyblish-starter requires pyblish-maya.\
-+""")
-+
-+ messagebox.setDetailedText("""\
-+1) Install Pyblish for Maya
-+
-+ $ pip install pyblish-maya
-+
-+2) Run setup()
-+
-+ >>> import pyblish_maya
-+ >>> pyblish_maya.setup()
-+
-+3) Try again.
-+
-+ >>> pyblish_starter.install()
-+
-+See https://github.com/pyblish/pyblish-starter for more information.
-+""")
-+
-+ messagebox.setStandardButtons(messagebox.Ok)
-+ messagebox.exec_()
-+
-+ raise RuntimeError("pyblish-starter requires pyblish-maya "
-+ "to have been setup.")
-diff --git a/pyblish_starter/pipeline.py b/pyblish_starter/pipeline.py
-index 12ff3dd..5490e7d 100644
---- a/pyblish_starter/pipeline.py
-+++ b/pyblish_starter/pipeline.py
-@@ -1,28 +1,43 @@
- import os
--import re
- import sys
-+import json
- import types
- import logging
--import datetime
-
- from pyblish import api
-
-+from . import lib
-+
- self = sys.modules[__name__]
-
-+self.log = logging.getLogger()
-+
- self._registered_data = list()
- self._registered_families = list()
-+self._registered_formats = list()
-+
-+
-+def default_host():
-+ """A default host, in place of anything better
-+
-+ This may be considered as reference for the
-+ interface a host must implement. It also ensures
-+ that the system runs, even when nothing is there
-+ to support it.
-
--self._log = logging.getLogger()
-+ """
-+
-+ host = types.ModuleType("default")
-+ host.__dict__.update({
-+ "ls": lambda: ["Asset1", "Asset2"],
-+ "loader": lambda asset, version, representation: None,
-+ "creator": lambda name, family: "my_instance",
-+ "supported_formats": lambda: [".ma", ".mb"]
-+ })
-
--# Mock host interface
--host = types.ModuleType("default")
--host.__dict__.update({
-- "ls": lambda: ["Asset1", "Asset2"],
-- "loader": lambda asset, version, representation: None,
-- "creator": lambda name, family: "my_instance"
--})
-+ return host
-
--self._registered_host = host
-+self._registered_host = default_host()
-
-
- def install(host):
-@@ -42,88 +57,151 @@ def install(host):
-
- register_host(host)
- register_plugins()
-+ register_default_data()
-+ register_default_families()
-
-- register_data(key="id", value="pyblish.starter.instance")
-- register_data(key="label", value="{name}")
-- register_data(key="family", value="{family}")
--
-- register_family(
-- name="starter.model",
-- help="Polygonal geometry for animation"
-- )
-
-- register_family(
-- name="starter.rig",
-- help="Character rig"
-- )
-+def uninstall():
-+ try:
-+ registered_host().uninstall()
-+ except AttributeError:
-+ pass
-
-- register_family(
-- name="starter.animation",
-- help="Pointcache"
-- )
-+ deregister_host()
-+ deregister_plugins()
-+ deregister_default_data()
-+ deregister_default_families()
-
-
- def ls():
-- """List available assets"""
-- root = self.registered_host().root()
-- dirname = os.path.join(root, "public")
-- self._log.debug("Listing %s" % dirname)
--
-- try:
-- return os.listdir(dirname)
-- except OSError:
-- return list()
-+ """List available assets
-+
-+ Return a list of available assets.
-+
-+ Schema:
-+ {
-+ "schema": "pyblish-starter:asset-1.0",
-+ "name": Name of directory,
-+ "versions": [
-+ {
-+ "version": 1,
-+ "comment": "",
-+ "representations": [
-+ {
-+ "format": File extension,
-+ "path": Filename
-+ }
-+ ]
-+ },
-+ ]
-+ }
-+
-+ The interface of this function, along with its schema, is designed
-+ to facilitate a potential transition into database-driven queries.
-+
-+ A note on performance:
-+ This function is a generator, it scans the system one asset
-+ at a time. However, scanning implies both listing directories
-+ and opening files - one per asset per version.
-+
-+ Therefore, performance drops combinatorially for each new
-+ version added to the project.
-+
-+ In small pipelines - e.g. 100s of assets, with 10s of versions -
-+ this should not pose a problem.
-+
-+ In large pipelines - e.g. 1000s of assets, with 100s of versions -
-+ this would likely become unbearable and manifest itself in
-+ surrounding areas of the pipeline where disk-access is
-+ critical; such as saving or loading files.
-+
-+ ..note: The order of the list is undefined, but is typically alphabetical
-+ due to how os.listdir() is implemented.
-+
-+ ..note: The order of versions returned is guaranteed to be sorted, so
-+ as to simplify retrieving the latest one via `versions[-1]`
-
-+ """
-
--def abspath(asset, version=-1, representation=None):
- root = registered_host().root()
-+ assetsdir = os.path.join(root, "public")
-
-- dirname = os.path.join(
-- root,
-- "public",
-- asset
-- )
-+ for asset in os.listdir(assetsdir):
-+ versionsdir = os.path.join(assetsdir, asset)
-
-- try:
-- versions = os.listdir(dirname)
-- except OSError:
-- raise OSError("\"%s\" not found." % asset)
-+ asset_entry = {
-+ "schema": "pyblish-starter:asset-1.0",
-+ "name": asset,
-+ "versions": list()
-+ }
-
-- # Automatically deduce version
-- if version == -1:
-- version = find_latest_version(versions)
-+ for version in os.listdir(versionsdir):
-+ versiondir = os.path.join(versionsdir, version)
-+ fname = os.path.join(versiondir, ".metadata.json")
-
-- dirname = os.path.join(
-- dirname,
-- "v%03d" % version
-- )
-+ try:
-+ with open(fname) as f:
-+ data = json.load(f)
-
-- try:
-- representations = dict()
-- for fname in os.listdir(dirname):
-- name, ext = os.path.splitext(fname)
-- representations[ext] = fname
-+ except IOError:
-+ self.log.warning("\"%s\" not found." % fname)
-+ continue
-+
-+ if data.get("schema") != "pyblish-starter:version-1.0":
-+ self.log.warning("\"%s\" unsupported schema." % fname)
-+ continue
-+
-+ version_entry = {
-+ "version": lib.parse_version(version),
-+ "path": versiondir,
-+ "representations": list()
-+ }
-+
-+ for representation in os.listdir(versiondir):
-+ if representation.startswith("."):
-+ continue
-+
-+ name, ext = os.path.splitext(representation)
-+ version_entry["representations"].append({
-+ "format": ext,
-+ "path": "{dirname}/%s{format}" % name
-+ })
-+
-+ asset_entry["versions"].append(version_entry)
-
-- if not representations:
-- raise OSError
-+ # Sort versions by integer
-+ asset_entry["versions"].sort(key=lambda v: v["version"])
-
-- except OSError:
-- raise OSError("v%03d of \"%s\" not found." % (version, asset))
-+ yield asset_entry
-
-- # Automatically deduce representation
-- if representation is None:
-- fname = representations.values()[0]
-
-- return os.path.join(
-- dirname,
-- fname
-+def register_default_data():
-+ register_data(key="id", value="pyblish.starter.instance")
-+ register_data(key="label", value="{name}")
-+ register_data(key="family", value="{family}")
-+
-+
-+def register_default_families():
-+ register_family(
-+ name="starter.model",
-+ help="Polygonal geometry for animation"
-+ )
-+
-+ register_family(
-+ name="starter.rig",
-+ help="Character rig"
-+ )
-+
-+ register_family(
-+ name="starter.animation",
-+ help="Pointcache"
- )
-
-
- def register_host(host):
- for member in ("root",
- "loader",
-- "creator"):
-+ "creator",):
- assert hasattr(host, member), "Missing %s" % member
-
- self._registered_host = host
-@@ -171,77 +249,39 @@ def register_family(name, data=None, help=None):
- })
-
-
-+def registered_formats():
-+ return self._registered_formats[:]
-+
-+
- def registered_families():
-- return list(self._registered_families)
-+ return self._registered_families[:]
-
-
- def registered_data():
-- return list(self._registered_data)
-+ return self._registered_data[:]
-
-
- def registered_host():
- return self._registered_host
-
-
--def time():
-- return datetime.datetime.now().strftime("%Y-%m-%dT%H-%M-%SZ")
--
--
--def format_private_dir(root, name):
-- dirname = os.path.join(root, "private", time(), name)
-- return dirname
--
--
--def find_latest_version(versions):
-- """Return latest version from list of versions
--
-- If multiple numbers are found in a single version,
-- the last one found is used. E.g. (6) from "v7_22_6"
--
-- Arguments:
-- versions (list): Version numbers as string
--
-- Example:
-- >>> find_next_version(["v001", "v002", "v003"])
-- 4
-- >>> find_next_version(["1", "2", "3"])
-- 4
-- >>> find_next_version(["v1", "v0002", "verision_3"])
-- 4
-- >>> find_next_version(["v2", "5_version", "verision_8"])
-- 9
-- >>> find_next_version(["v2", "v3_5", "_1_2_3", "7, 4"])
-- 6
-- >>> find_next_version(["v010", "v011"])
-- 12
--
-- """
-+def deregister_default_families():
-+ self._registered_families[:] = list()
-
-- highest_version = 0
-- for version in versions:
-- matches = re.findall(r"\d+", version)
-
-- if not matches:
-- continue
-+def deregister_default_data():
-+ self._registered_data[:] = list()
-
-- version = int(matches[-1])
-- if version > highest_version:
-- highest_version = version
-
-- return highest_version
--
--
--def find_next_version(versions):
-- """Return next version from list of versions
--
-- See docstring for :func:`find_latest_version`.
--
-- Arguments:
-- versions (list): Version numbers as string
-+def deregister_plugins():
-+ from . import plugins
-+ plugin_path = os.path.dirname(plugins.__file__)
-
-- Returns:
-- int: Next version number
-+ try:
-+ api.deregister_plugin_path(plugin_path)
-+ except ValueError:
-+ self.log.warning("pyblish-starter plug-ins not registered.")
-
-- """
-
-- return find_latest_version(versions) + 1
-+def deregister_host():
-+ self._registered_host = default_host()
-diff --git a/pyblish_starter/plugins/collect_instances.py b/pyblish_starter/plugins/collect_instances.py
-index 1508bb2..e63e6c5 100644
---- a/pyblish_starter/plugins/collect_instances.py
-+++ b/pyblish_starter/plugins/collect_instances.py
-@@ -44,12 +44,15 @@ class CollectStarterInstances(api.ContextPlugin):
- from maya import cmds
-
- try:
-+ # Assertion also made in pyblish_starter.install()
-+ # but as plug-ins can be used vanilla, the check
-+ # must also be made here.
- import pyblish_maya
- assert pyblish_maya.is_setup()
-
- except (ImportError, AssertionError):
- raise RuntimeError("pyblish-starter requires pyblish-maya "
-- "to have been install.")
-+ "to have been setup.")
-
- for objset in cmds.ls("*.id",
- long=True, # Produce full names
-diff --git a/pyblish_starter/plugins/extract_animation.py b/pyblish_starter/plugins/extract_animation.py
-index c232c9b..9dbc694 100644
---- a/pyblish_starter/plugins/extract_animation.py
-+++ b/pyblish_starter/plugins/extract_animation.py
-@@ -48,5 +48,6 @@ class ExtractStarterAnimation(api.InstancePlugin):
-
- # Store reference for integration
- instance.data["privateDir"] = dirname
-+ instance.data["filename"] = filename
-
- self.log.info("Extracted {instance} to {dirname}".format(**locals()))
-diff --git a/pyblish_starter/plugins/extract_model.py b/pyblish_starter/plugins/extract_model.py
-index 704b9d2..54d6746 100644
---- a/pyblish_starter/plugins/extract_model.py
-+++ b/pyblish_starter/plugins/extract_model.py
-@@ -47,5 +47,6 @@ class ExtractStarterModel(api.InstancePlugin):
-
- # Store reference for integration
- instance.data["privateDir"] = dirname
-+ instance.data["filename"] = filename
-
- self.log.info("Extracted {instance} to {path}".format(**locals()))
-diff --git a/pyblish_starter/plugins/extract_rig.py b/pyblish_starter/plugins/extract_rig.py
-index f107040..4766a26 100644
---- a/pyblish_starter/plugins/extract_rig.py
-+++ b/pyblish_starter/plugins/extract_rig.py
-@@ -51,5 +51,6 @@ class ExtractStarterRig(api.InstancePlugin):
-
- # Store reference for integration
- instance.data["privateDir"] = dirname
-+ instance.data["filename"] = filename
-
- self.log.info("Extracted {instance} to {path}".format(**locals()))
-diff --git a/pyblish_starter/plugins/integrate_asset.py b/pyblish_starter/plugins/integrate_asset.py
-index 6ae21f9..032554e 100644
---- a/pyblish_starter/plugins/integrate_asset.py
-+++ b/pyblish_starter/plugins/integrate_asset.py
-@@ -14,7 +14,9 @@ class IntegrateStarterAsset(api.InstancePlugin):
-
- def process(self, instance):
- import os
-+ import json
- import shutil
-+ import pyblish_starter
-
- privatedir = instance.data.get("privateDir")
- assert privatedir, (
-@@ -30,11 +32,38 @@ class IntegrateStarterAsset(api.InstancePlugin):
- except OSError:
- pass
-
-- versions = len(os.listdir(instancedir))
-- next_version = "v%03d" % (versions + 1)
-- versiondir = os.path.join(instancedir, next_version)
-+ version = len(os.listdir(instancedir)) + 1
-+ versiondir = os.path.join(
-+ instancedir,
-+ pyblish_starter.format_version(version)
-+ )
-
- shutil.copytree(privatedir, versiondir)
-
-- self.log.info("Successfully integrated %s to %s" % (
-+ # Update metadata
-+ fname = os.path.join(versiondir, ".metadata.json")
-+
-+ try:
-+ with open(fname) as f:
-+ metadata = json.load(f)
-+ except IOError:
-+ metadata = {
-+ "schema": "pyblish-starter:version-1.0",
-+ "version": version,
-+ "representations": list()
-+ }
-+
-+ filename = instance.data["filename"]
-+ name, ext = os.path.splitext(filename)
-+ metadata["representations"].append(
-+ {
-+ "format": ext,
-+ "path": "{version}/%s" % filename
-+ }
-+ )
-+
-+ with open(fname, "w") as f:
-+ json.dump(metadata, f)
-+
-+ self.log.info("Successfully integrated \"%s\" to \"%s\"" % (
- instance, versiondir))
-diff --git a/pyblish_starter/tests/test_pipeline.py b/pyblish_starter/tests/test_pipeline.py
-index 5c1b470..3c4ff46 100644
---- a/pyblish_starter/tests/test_pipeline.py
-+++ b/pyblish_starter/tests/test_pipeline.py
-@@ -1,16 +1,21 @@
- import os
- import sys
-+import json
-+import types
- import shutil
- import tempfile
-
- import pyblish_starter
-
-+from nose.tools import assert_equals
-+
- self = sys.modules[__name__]
-
-
- def setup():
- self.tempdir = tempfile.mkdtemp()
-- sys.stdout.write("Created temporary directory \"%s\"" % self.tempdir)
-+ _register_host()
-+ _generate_fixture()
-
-
- def teardown():
-@@ -18,16 +23,125 @@ def teardown():
- sys.stdout.write("Removed temporary directory \"%s\"" % self.tempdir)
-
-
--def test_ls():
-- """ls() returns available assets from current root directory"""
-+def _register_host():
-+ host = types.ModuleType("Test")
-+ host.__dict__.update({
-+ "root": lambda: self.tempdir,
-+ "creator": lambda *args, **kwargs: None,
-+ "loader": lambda *args, **kwargs: None,
-+ "supported_formats": [".ma"]
-+ })
-+
-+ pyblish_starter.register_host(host)
-+
-+
-+def _generate_fixture():
- root = os.path.join(
- self.tempdir,
- "public"
- )
--
-- for asset in ("Asset1", "Asset2"):
-- os.makedirs(os.path.join(root, asset))
--
-- pyblish_starter.register_root(self.tempdir)
--
-- assert pyblish_starter.ls() == ["Asset1", "Asset2"]
-+
-+ for asset in ("Asset1",):
-+ assetdir = os.path.join(root, asset)
-+ os.makedirs(assetdir)
-+
-+ if asset == "BadAsset":
-+ # An asset must have at least one version
-+ continue
-+
-+ for version in ("v001",):
-+ versiondir = os.path.join(assetdir, version)
-+ os.makedirs(versiondir)
-+
-+ fname = os.path.join(versiondir, asset + ".ma")
-+ open(fname, "w").close() # touch
-+
-+ fname = os.path.join(versiondir, ".metadata.json")
-+
-+ with open(fname, "w") as f:
-+ json.dump({
-+ "schema": "pyblish-starter:version-1.0",
-+ "name": asset,
-+ "path": versiondir,
-+ "representations": [
-+ {
-+ "format": ".ma",
-+ "source": "{project}/maya/scenes/scene.ma",
-+ "author": "marcus",
-+ "path": "{dirname}/%s{format}" % asset,
-+ },
-+ ]
-+ }, f)
-+
-+
-+def test_ls():
-+ """ls() returns available assets from current root directory
-+
-+ ls() returns a formatted list of available assets. For an asset
-+ to be recognised as an asset, it must adhere to a strict schema.
-+
-+ ________________________________ ________________________________
-+ | | | | | | |
-+ | version1 | version2 | version3 | version1 | version2 | version3 |
-+ |__________|__________|__________|__________|__________|__________|
-+ | | |
-+ | asset1 | asset2 |
-+ |________________________________|________________________________|
-+ | |
-+ | project |
-+ |_________________________________________________________________|
-+
-+ This schema is located within each version of an asset and is
-+ denoted `pyblish-starter:version-1.0`.
-+
-+ The members of this schema is also strict, they are:
-+
-+ {
-+ "schema": "pyblish-starter:version-1.0",
-+ "name": "Name of asset",
-+ "representations": [List of representations],
-+ }
-+
-+ Where each `representation` follows the following schema.
-+
-+ {
-+ "format": "file extension or similar identifier",
-+ "source": "absolute path to source file",
-+ "author": "original author of version",
-+ "path": "relative path to file, starting at {root}"
-+ }
-+
-+ The returned dictionary is also strictly defined.
-+
-+ """
-+
-+ asset = next(pyblish_starter.ls())
-+ reference = {
-+ "schema": "pyblish-starter:asset-1.0",
-+ "name": "Asset1",
-+ "versions": [
-+ {
-+ "version": 1,
-+ "path": os.path.join(self.tempdir, "public", "Asset1/v001"),
-+ "representations": [
-+ {
-+ "format": ".ma",
-+ "path": "{dirname}/Asset1{format}"
-+ }
-+ ]
-+ },
-+ ]
-+ }
-+
-+ # Printed on error
-+ print("# Comparing result:")
-+ print(json.dumps(asset, indent=4, sort_keys=True))
-+ print("# With reference:")
-+ print(json.dumps(reference, indent=4, sort_keys=True))
-+
-+ assert_equals(asset, reference)
-+
-+
-+def test_ls_returns_sorted_versions():
-+ """Versions returned from ls() are alphanumerically sorted"""
-+ assert False
-diff --git a/pyblish_starter/tools/asset_loader/app.py b/pyblish_starter/tools/asset_loader/app.py
-index 2ee7f46..05a5c34 100644
---- a/pyblish_starter/tools/asset_loader/app.py
-+++ b/pyblish_starter/tools/asset_loader/app.py
-@@ -9,7 +9,36 @@ self = sys.modules[__name__]
- self._window = None
-
-
-+# Custom roles
-+ItemRole = QtCore.Qt.UserRole + 1
-+
-+
- class Window(QtWidgets.QDialog):
-+ """Basic asset loader interface
-+
-+ _________________________
-+ | |
-+ | Assets |
-+ | ______________________ |
-+ | | | |
-+ | | Asset 1 | |
-+ | | Asset 2 | |
-+ | | ... | |
-+ | | | |
-+ | | | |
-+ | | | |
-+ | | | |
-+ | | | |
-+ | | | |
-+ | |______________________| |
-+ | ______________________ |
-+ | | | |
-+ | | Load | |
-+ | |______________________| |
-+ |__________________________|
-+
-+ """
-+
- def __init__(self, parent=None):
- super(Window, self).__init__(parent)
- self.setWindowTitle("Asset Loader")
-@@ -91,15 +120,17 @@ class Window(QtWidgets.QDialog):
- def refresh(self):
- listing = self.data["model"]["listing"]
-
-- assets = pipeline.ls()
-+ has_assets = False
-+
-+ for asset in pipeline.ls():
-+ item = QtWidgets.QListWidgetItem(asset["name"])
-+ item.setData(QtCore.Qt.ItemIsEnabled, True)
-+ item.setData(ItemRole, asset)
-+ listing.addItem(item)
-
-- if assets:
-- for asset in assets:
-- item = QtWidgets.QListWidgetItem(asset)
-- item.setData(QtCore.Qt.ItemIsEnabled, True)
-- listing.addItem(item)
-+ has_assets = True
-
-- else:
-+ if not has_assets:
- item = QtWidgets.QListWidgetItem("No assets found")
- item.setData(QtCore.Qt.ItemIsEnabled, False)
- listing.addItem(item)
-@@ -114,15 +145,23 @@ class Window(QtWidgets.QDialog):
- item = listing.currentItem()
-
- if item is not None:
-+ asset = item.data(ItemRole)
-+
- try:
-- pipeline.registered_host().loader(item.text())
-+ pipeline.registered_host().loader(asset)
-+
-+ except ValueError as e:
-+ error_msg.setText(str(e))
-+ error_msg.show()
-+ raise
-
- except NameError as e:
- error_msg.setText(str(e))
- error_msg.show()
- raise
-
-- except (TypeError, RuntimeError, KeyError) as e:
-+ # Catch-all
-+ except Exception as e:
- error_msg.setText("Program error: %s" % str(e))
- error_msg.show()
- raise
-diff --git a/run_tests.py b/run_tests.py
-index 64dfb75..8a3c366 100644
---- a/run_tests.py
-+++ b/run_tests.py
-@@ -1,6 +1,8 @@
- import sys
--import time
- import types
-+import warnings
-+
-+warnings.filterwarnings("ignore", category=DeprecationWarning)
-
- maya = types.ModuleType("maya")
- maya.mel = types.ModuleType("mel")
-diff --git a/setup.py b/setup.py
-index 8deb45d..719b808 100644
---- a/setup.py
-+++ b/setup.py
-@@ -46,7 +46,7 @@ setup(
- classifiers=classifiers,
- install_requires=[
- "pyblish-base>=1.4",
-- "pyblish-maya>=2.0"
-+ "pyblish-maya>=2.1"
- ],
- entry_points={},
- )
---
-1.7.1
-
diff --git a/pyblish_starter/__init__.py b/pyblish_starter/__init__.py
index ae1fecf..970bc3a 100644
--- a/pyblish_starter/__init__.py
+++ b/pyblish_starter/__init__.py
@@ -19,6 +19,8 @@
format_user_dir,
format_shared_dir,
format_version,
+
+ find_next_version,
parse_version,
)
@@ -35,5 +37,7 @@
"format_user_dir",
"format_shared_dir",
"format_version",
+
+ "find_next_version",
"parse_version",
]
diff --git a/pyblish_starter/lib.py b/pyblish_starter/lib.py
index d75e965..9937795 100644
--- a/pyblish_starter/lib.py
+++ b/pyblish_starter/lib.py
@@ -26,7 +26,7 @@ def listdir(dirname):
def time():
"""Return file-system safe string of current date and time"""
- return datetime.datetime.now().strftime("%Y-%m-%dT%H-%M-%SZ")
+ return datetime.datetime.now().strftime("%Y%m%dT%H%M%SZ")
def format_shared_dir(root):
diff --git a/pyblish_starter/maya/lib.py b/pyblish_starter/maya/lib.py
index 52aabee..dbedece 100644
--- a/pyblish_starter/maya/lib.py
+++ b/pyblish_starter/maya/lib.py
@@ -80,3 +80,32 @@ def export_alembic(nodes, file, frame_range=None, uv_write=True):
mel_cmd = "AbcExport -j \"{0}\"".format(mel_args_string)
return mel.eval(mel_cmd)
+
+
+def imprint(node, data):
+ """Write `data` to `node` as userDefined attributes
+
+ Arguments:
+ node (str): Long name of node
+ data (dict): Dictionary of key/value pairs
+
+ """
+
+ for key, value in data.items():
+ if isinstance(value, bool):
+ add_type = {"attributeType": "bool"}
+ set_type = {"keyable": False, "channelBox": True}
+ elif isinstance(value, basestring):
+ add_type = {"dataType": "string"}
+ set_type = {"type": "string"}
+ elif isinstance(value, int):
+ add_type = {"attributeType": "long"}
+ set_type = {"keyable": False, "channelBox": True}
+ elif isinstance(value, float):
+ add_type = {"attributeType": "double"}
+ set_type = {"keyable": False, "channelBox": True}
+ else:
+ raise TypeError("Unsupported type: %r" % type(value))
+
+ cmds.addAttr(node, longName=key, **add_type)
+ cmds.setAttr(node + "." + key, value, **set_type)
diff --git a/pyblish_starter/maya/pipeline.py b/pyblish_starter/maya/pipeline.py
index 1fb76cf..cb8f59a 100644
--- a/pyblish_starter/maya/pipeline.py
+++ b/pyblish_starter/maya/pipeline.py
@@ -7,6 +7,8 @@
from maya import cmds, mel
+from . import lib
+
self = sys.modules[__name__]
self.log = logging.getLogger()
self.menu = "pyblishStarter"
@@ -76,15 +78,17 @@ def root():
def load(asset, version=-1):
"""Load asset
+
+
Arguments:
- asset (dict): Object of schema "pyblish-starter:asset-1.0"
+ asset ("pyblish-starter:asset-1.0"): Asset which to import
version (int, optional): Version number, defaults to latest
Returns:
Reference node
Raises:
- IndexError on version not found
+ IndexError on no version found
ValueError on no supported representation
"""
@@ -104,15 +108,17 @@ def load(asset, version=-1):
# Such as choosing between `.obj` and `.ma` and `.abc`,
# each compatible but different.
try:
- representation = next(rep for rep in version["representations"]
- if rep["format"] in supported_formats)
+ representation = next(
+ rep for rep in version["representations"]
+ if rep["format"] in supported_formats
+ )
+
except StopIteration:
+ formats = list(r["format"] for r in version["representations"])
raise ValueError(
- "No supported representations for %s\n"
- "Supported representations: %s" % (
- asset["name"],
- ", ".join(r["format"] for r in version["representations"]))
- )
+ "No supported representations for \"%s\"\n\n"
+ "Supported representations: %s"
+ % (asset["name"], "\n- ".join(formats)))
fname = representation["path"].format(
dirname=version["path"],
@@ -121,18 +127,30 @@ def load(asset, version=-1):
nodes = cmds.file(fname,
namespace=asset["name"] + "_",
- reference=True)
+ reference=True,
+ returnNewNodes=True)
+
+ self.log.info("Containerising \"%s\".." % fname)
+ containerise(asset["name"], nodes, version)
- return cmds.referenceQuery(nodes, referenceNode=True)
+ self.log.info("Container created, returning reference node.")
+ return cmds.referenceQuery(nodes[0], referenceNode=True)
def create(name, family):
"""Create new instance
+ Associate nodes with a name and family. These nodes are later
+ validated, according to their `family`, and integrated into the
+ shared environment, relative their `name`.
+
+ Data relative each family, along with default data, are imprinted
+ into the resulting objectSet. This data is later used by extractors
+ and finally asset browsers to help identify the origin of the asset.
+
Arguments:
name (str): Name of instance
family (str): Name of family
- use_selection (bool): Use selection to create this instance?
"""
@@ -145,42 +163,28 @@ def create(name, family):
print("%s + %s" % (pipeline.registered_data(), item.get("data", [])))
data = pipeline.registered_data() + item.get("data", [])
+
+ # Convert to dictionary
+ data = dict((i["key"], i["value"]) for i in data)
+
instance = "%s_SEL" % name
if cmds.objExists(instance):
raise NameError("\"%s\" already exists." % instance)
- # Include selection
instance = cmds.sets(name=instance)
- for item in data:
- key = item["key"]
-
+ # Resolve template
+ for key, value in data.items():
try:
- value = item["value"].format(
+ data[key] = value.format(
name=name,
family=family
)
except KeyError as e:
raise KeyError("Invalid dynamic property: %s" % e)
- if isinstance(value, bool):
- add_type = {"attributeType": "bool"}
- set_type = {"keyable": False, "channelBox": True}
- elif isinstance(value, basestring):
- add_type = {"dataType": "string"}
- set_type = {"type": "string"}
- elif isinstance(value, int):
- add_type = {"attributeType": "long"}
- set_type = {"keyable": False, "channelBox": True}
- elif isinstance(value, float):
- add_type = {"attributeType": "double"}
- set_type = {"keyable": False, "channelBox": True}
- else:
- raise TypeError("Unsupported type: %r" % type(value))
-
- cmds.addAttr(instance, ln=key, **add_type)
- cmds.setAttr(instance + "." + key, value, **set_type)
+ lib.imprint(instance, data)
cmds.select(instance, noExpand=True)
@@ -190,6 +194,39 @@ def create(name, family):
return instance
+def containerise(name, nodes, version):
+ """Bundle `nodes` into an assembly and imprint it with metadata
+
+ Containerisation enables a tracking of version, author and origin
+ for loaded assets.
+
+ Arguments:
+ name (str): Name of resulting assembly
+ nodes (list): Long names of nodes to containerise
+ version (pyblish-starter:version-1.0): Current version
+
+ """
+
+ assemblies = cmds.ls(nodes, assemblies=True)
+ container = cmds.group(assemblies, name=name)
+
+ for key, value in (
+ ("id", "pyblish.starter.container"),
+ ("author", version["author"]),
+ ("loader", self.__name__),
+ ("time", version["time"]),
+ ("comment", version.get("comment", "")),
+ ):
+
+ if not value:
+ continue
+
+ cmds.addAttr(container, longName=key, dataType="string")
+ cmds.setAttr(container + "." + key, value, type="string")
+
+ return container
+
+
def _display_missing_dependencies():
import pyblish
@@ -212,26 +249,27 @@ def _display_missing_dependencies():
messagebox.setWindowTitle("Uh oh")
messagebox.setText("Missing dependencies")
- messagebox.setInformativeText("""\
-pyblish-starter requires pyblish-maya.\
-""")
-
- messagebox.setDetailedText("""\
-1) Install Pyblish for Maya
-
- $ pip install pyblish-maya
-
-2) Run setup()
-
- >>> import pyblish_maya
- >>> pyblish_maya.setup()
-
-3) Try again.
-
- >>> pyblish_starter.install()
+ messagebox.setInformativeText(
+ "pyblish-starter requires pyblish-maya.\n"
+ )
-See https://github.com/pyblish/pyblish-starter for more information.
-""")
+ messagebox.setDetailedText(
+ "1) Install Pyblish for Maya\n"
+ "\n"
+ "$ pip install pyblish-maya\n"
+ "\n"
+ "2) Run setup()\n"
+ "\n"
+ ">>> import pyblish_maya\n"
+ ">>> pyblish_maya.setup()\n"
+ "\n"
+ "3) Try again.\n"
+ "\n"
+ ">>> pyblish_starter.install()\n"
+
+ "See https://github.com/pyblish/pyblish-starter "
+ "for more information."
+ )
messagebox.setStandardButtons(messagebox.Ok)
messagebox.exec_()
diff --git a/pyblish_starter/pipeline.py b/pyblish_starter/pipeline.py
index 97bae94..6f96ede 100644
--- a/pyblish_starter/pipeline.py
+++ b/pyblish_starter/pipeline.py
@@ -95,6 +95,29 @@ def uninstall():
self.log.info("Successfully uninstalled Pyblish Starter!")
+def register_default_data():
+ register_data(key="id", value="pyblish.starter.instance")
+ register_data(key="name", value="{name}")
+ register_data(key="family", value="{family}")
+
+
+def register_default_families():
+ register_family(
+ name="starter.model",
+ help="Polygonal geometry for animation"
+ )
+
+ register_family(
+ name="starter.rig",
+ help="Character rig"
+ )
+
+ register_family(
+ name="starter.animation",
+ help="Pointcache"
+ )
+
+
def ls():
"""List available assets
@@ -181,29 +204,6 @@ def ls():
yield asset_entry
-def register_default_data():
- register_data(key="id", value="pyblish.starter.instance")
- register_data(key="name", value="{name}")
- register_data(key="family", value="{family}")
-
-
-def register_default_families():
- register_family(
- name="starter.model",
- help="Polygonal geometry for animation"
- )
-
- register_family(
- name="starter.rig",
- help="Character rig"
- )
-
- register_family(
- name="starter.animation",
- help="Pointcache"
- )
-
-
def register_format(format):
self._registered_formats.append(format)
diff --git a/pyblish_starter/plugins/collect_instances.py b/pyblish_starter/plugins/collect_instances.py
index e63e6c5..c306280 100644
--- a/pyblish_starter/plugins/collect_instances.py
+++ b/pyblish_starter/plugins/collect_instances.py
@@ -61,7 +61,6 @@ def process(self, context):
objectsOnly=True): # Return objectSet, rather
# than its members
- # print("cmds: %s" % cmds)
if not cmds.objExists(objset + ".id"):
continue
@@ -86,10 +85,10 @@ def process(self, context):
# such as mesh and color attributes. These
# are considered non-essential to this
# particular publishing pipeline.
- continue
+ value = None
instance.data[attr] = value
# Produce diagnostic message for any graphical
# user interface interested in visualising it.
- self.log.info("Found: %s " % objset)
+ self.log.info("Found: \"%s\" " % instance.data["name"])
diff --git a/pyblish_starter/plugins/extract_animation.py b/pyblish_starter/plugins/extract_animation.py
index f11deb4..bd3e3b7 100644
--- a/pyblish_starter/plugins/extract_animation.py
+++ b/pyblish_starter/plugins/extract_animation.py
@@ -1,5 +1,4 @@
from pyblish import api
-import pyblish_starter as starter
class ExtractStarterAnimation(api.InstancePlugin):
@@ -21,13 +20,14 @@ class ExtractStarterAnimation(api.InstancePlugin):
def process(self, instance):
import os
from maya import cmds
+ from pyblish_starter import format_user_dir
from pyblish_starter.maya import export_alembic
self.log.debug("Loading plug-in..")
cmds.loadPlugin("AbcExport.mll", quiet=True)
self.log.info("Extracting animation..")
- dirname = starter.format_user_dir(
+ dirname = format_user_dir(
root=instance.context.data["workspaceDir"],
name=instance.data["name"])
@@ -36,7 +36,7 @@ def process(self, instance):
except OSError:
pass
- filename = "%s.abc" % instance
+ filename = "{name}.ma".format(**instance.data)
export_alembic(
nodes=instance,
@@ -47,7 +47,9 @@ def process(self, instance):
)
# Store reference for integration
- instance.data["userDir"] = dirname
- instance.data["filename"] = filename
+ instance.data.update({
+ "userDir": dirname,
+ "filename": filename,
+ })
self.log.info("Extracted {instance} to {dirname}".format(**locals()))
diff --git a/pyblish_starter/plugins/extract_model.py b/pyblish_starter/plugins/extract_model.py
index 05ea344..6f0840a 100644
--- a/pyblish_starter/plugins/extract_model.py
+++ b/pyblish_starter/plugins/extract_model.py
@@ -1,5 +1,4 @@
from pyblish import api
-import pyblish_starter as starter
class ExtractStarterModel(api.InstancePlugin):
@@ -11,7 +10,7 @@ class ExtractStarterModel(api.InstancePlugin):
"""
- label = "Extract starter model"
+ label = "Extract model"
order = api.ExtractorOrder
hosts = ["maya"]
families = ["starter.model"]
@@ -19,9 +18,10 @@ class ExtractStarterModel(api.InstancePlugin):
def process(self, instance):
import os
from maya import cmds
+ from pyblish_starter import format_user_dir
from pyblish_maya import maintained_selection
- dirname = starter.format_user_dir(
+ dirname = format_user_dir(
root=instance.context.data["workspaceDir"],
name=instance.data["name"])
@@ -30,7 +30,7 @@ def process(self, instance):
except OSError:
pass
- filename = "%s.ma" % instance
+ filename = "{name}.ma".format(**instance.data)
path = os.path.join(dirname, filename)
@@ -43,10 +43,16 @@ def process(self, instance):
typ="mayaAscii",
exportSelected=True,
preserveReferences=False,
+
+ # Construction history inherited from collection
+ # This enables a selective export of nodes relevant
+ # to this particular plug-in.
constructionHistory=False)
# Store reference for integration
- instance.data["userDir"] = dirname
- instance.data["filename"] = filename
+ instance.data.update({
+ "userDir": dirname,
+ "filename": filename,
+ })
self.log.info("Extracted {instance} to {path}".format(**locals()))
diff --git a/pyblish_starter/plugins/extract_rig.py b/pyblish_starter/plugins/extract_rig.py
index ae8067e..c6c2e23 100644
--- a/pyblish_starter/plugins/extract_rig.py
+++ b/pyblish_starter/plugins/extract_rig.py
@@ -1,5 +1,4 @@
from pyblish import api
-import pyblish_starter as starter
class ExtractStarterRig(api.InstancePlugin):
@@ -21,11 +20,11 @@ class ExtractStarterRig(api.InstancePlugin):
def process(self, instance):
import os
-
from maya import cmds
+ from pyblish_starter import format_user_dir
from pyblish_maya import maintained_selection
- dirname = starter.format_user_dir(
+ dirname = format_user_dir(
root=instance.context.data["workspaceDir"],
name=instance.data["name"])
@@ -34,7 +33,7 @@ def process(self, instance):
except OSError:
pass
- filename = "%s.ma" % instance
+ filename = "{name}.ma".format(**instance.data)
path = os.path.join(dirname, filename)
@@ -50,7 +49,9 @@ def process(self, instance):
constructionHistory=True)
# Store reference for integration
- instance.data["userDir"] = dirname
- instance.data["filename"] = filename
+ instance.data.update({
+ "userDir": dirname,
+ "filename": filename,
+ })
self.log.info("Extracted {instance} to {path}".format(**locals()))
diff --git a/pyblish_starter/plugins/integrate_asset.py b/pyblish_starter/plugins/integrate_asset.py
index 00797b1..1f79f0c 100644
--- a/pyblish_starter/plugins/integrate_asset.py
+++ b/pyblish_starter/plugins/integrate_asset.py
@@ -2,40 +2,54 @@
class IntegrateStarterAsset(api.InstancePlugin):
- """Publicise each instance
+ """Move user data to shared location
- Limitations:
- - Limited to publishing within a single Maya project
+ This plug-in exposes your data to others by encapsulating it
+ into a new version.
"""
label = "Integrate asset"
order = api.IntegratorOrder
+ families = [
+ "starter.model",
+ "starter.rig",
+ "starter.animation"
+ ]
def process(self, instance):
import os
import json
+ import errno
import shutil
- import pyblish_starter
+ from pyblish_starter import (
+ format_version,
+ find_next_version,
+ )
+
+ context = instance.context
userdir = instance.data.get("userDir")
assert userdir, (
"Incomplete instance \"%s\": "
"Missing reference to user directory."
- % instance)
+ % instance
+ )
- root = instance.context.data["workspaceDir"]
- instancedir = os.path.join(root, "shared", str(instance))
+ root = context.data["workspaceDir"]
+ instancedir = os.path.join(root, "shared", instance.data["name"])
try:
os.makedirs(instancedir)
- except OSError:
- pass
+ except OSError as e:
+ if e.errno != errno.EEXIST: # Already exists
+ self.log.critical("An unexpected error occurred.")
+ raise
- version = len(os.listdir(instancedir)) + 1
+ version = find_next_version(os.listdir(instancedir))
versiondir = os.path.join(
instancedir,
- pyblish_starter.format_version(version)
+ format_version(version)
)
shutil.copytree(userdir, versiondir)
@@ -51,7 +65,14 @@ def process(self, instance):
"schema": "pyblish-starter:version-1.0",
"version": version,
"path": versiondir,
- "representations": list()
+ "representations": list(),
+
+ # Collected by pyblish-base
+ "time": context.data["date"],
+ "author": context.data["user"],
+
+ # Collected by pyblish-maya
+ "source": context.data["currentFile"],
}
filename = instance.data["filename"]
diff --git a/pyblish_starter/plugins/validate_model_hierarchy.py b/pyblish_starter/plugins/validate_model_hierarchy.py
new file mode 100644
index 0000000..4c5c1cc
--- /dev/null
+++ b/pyblish_starter/plugins/validate_model_hierarchy.py
@@ -0,0 +1,20 @@
+from pyblish import api
+
+
+class ValidateStarterModelHierarchy(api.InstancePlugin):
+ """A model hierarchy must reside under a single assembly called "model_GRP"
+
+ - Must reside within `model_GRP` transform
+
+ """
+
+ label = "Validate model format"
+ order = api.ValidatorOrder
+ hosts = ["maya"]
+ families = ["starter.model"]
+
+ def process(self, instance):
+ from maya import cmds
+
+ assert cmds.ls(instance, assemblies=True) == ["model_GRP"], (
+ "Model must have a single parent called 'model_GRP'.")
diff --git a/pyblish_starter/plugins/validate_resources.py b/pyblish_starter/plugins/validate_resources.py
index 3ba3942..2e43325 100644
--- a/pyblish_starter/plugins/validate_resources.py
+++ b/pyblish_starter/plugins/validate_resources.py
@@ -1,7 +1,7 @@
from pyblish import api
-class ValidateResources(api.InstancePlugin):
+class ValidateStarterResources(api.InstancePlugin):
"""Resources must not contain absolute paths.
When working with external files, such as textures and references,
diff --git a/pyblish_starter/plugins/validate_rig_hierarchy.py b/pyblish_starter/plugins/validate_rig_hierarchy.py
new file mode 100644
index 0000000..71942a6
--- /dev/null
+++ b/pyblish_starter/plugins/validate_rig_hierarchy.py
@@ -0,0 +1,20 @@
+from pyblish import api
+
+
+class ValidateStarterRigHierarchy(api.InstancePlugin):
+ """A rig must reside under a single assembly called "rig_GRP"
+
+ - Must reside within `rig_GRP` transform
+
+ """
+
+ label = "Validate rig hierarchy"
+ order = api.ValidatorOrder
+ hosts = ["maya"]
+ families = ["starter.rig"]
+
+ def process(self, instance):
+ from maya import cmds
+
+ assert cmds.ls(instance, assemblies=True) == ["model_GRP"], (
+ "Rig must have a single parent called 'model_GRP'.")
diff --git a/pyblish_starter/plugins/validate_rig_members.py b/pyblish_starter/plugins/validate_rig_members.py
index c6ef7ab..cf0f133 100644
--- a/pyblish_starter/plugins/validate_rig_members.py
+++ b/pyblish_starter/plugins/validate_rig_members.py
@@ -1,16 +1,17 @@
from pyblish import api
-class ValidateStarterRigMembers(api.InstancePlugin):
- """A rig must have certain members
+class ValidateStarterRigFormat(api.InstancePlugin):
+ """A rig must have a certain hierarchy and members
+ - Must reside within `rig_GRP` transform
- controls_SEL
- cache_SEL
- resources_SEL (optional)
"""
- label = "Validate rig members"
+ label = "Validate rig format"
order = api.ValidatorOrder
hosts = ["maya"]
families = ["starter.rig"]
diff --git a/pyblish_starter/plugins/validate_single_assembly.py b/pyblish_starter/plugins/validate_single_assembly.py
new file mode 100644
index 0000000..1666042
--- /dev/null
+++ b/pyblish_starter/plugins/validate_single_assembly.py
@@ -0,0 +1,26 @@
+from pyblish import api
+
+
+class ValidateStarterSingleAssembly(api.InstancePlugin):
+ """A rig must have a certain hierarchy and members
+
+ - Must reside within `rig_GRP` transform
+ - controls_SEL
+ - cache_SEL
+ - resources_SEL (optional)
+
+ """
+
+ label = "Validate single assembly"
+ order = api.ValidatorOrder
+ hosts = ["maya"]
+ families = ["starter.model", "starter.rig"]
+
+ def process(self, instance):
+ from maya import cmds
+ assemblies = cmds.ls(instance, assemblies=True)
+ assert len(assemblies) == 1, (
+ ("Multiple assemblies found."
+ if len(assemblies) > 1
+ else "No assembly found")
+ )
diff --git a/pyblish_starter/tests/test_pipeline.py b/pyblish_starter/tests/test_pipeline.py
index eaf6f1f..bb3a3c7 100644
--- a/pyblish_starter/tests/test_pipeline.py
+++ b/pyblish_starter/tests/test_pipeline.py
@@ -6,7 +6,7 @@
import tempfile
import contextlib
-import pyblish_starter
+from pyblish_starter import pipeline, lib
from nose.tools import assert_equals
@@ -15,7 +15,7 @@
@contextlib.contextmanager
def root(root):
- host = pyblish_starter.registered_host()
+ host = pipeline.registered_host()
old = host.root
host.root = lambda: root
@@ -44,7 +44,7 @@ def _register_host():
"load": lambda *args, **kwargs: None,
})
- pyblish_starter.register_host(host)
+ pipeline.register_host(host)
def _generate_fixture():
@@ -57,10 +57,6 @@ def _generate_fixture():
assetdir = os.path.join(root, asset)
os.makedirs(assetdir)
- if asset == "BadAsset":
- # An asset must have at least one version
- continue
-
for version in ("v001",):
versiondir = os.path.join(assetdir, version)
os.makedirs(versiondir)
@@ -73,8 +69,10 @@ def _generate_fixture():
with open(fname, "w") as f:
json.dump({
"schema": "pyblish-starter:version-1.0",
- "version": pyblish_starter.parse_version(version),
+ "version": lib.parse_version(version),
"path": versiondir,
+ "time": "",
+ "author": "mottosso",
"representations": [
{
"format": ".ma",
@@ -134,7 +132,7 @@ def test_ls():
"""
- asset = next(pyblish_starter.ls())
+ asset = next(pipeline.ls())
reference = {
"schema": "pyblish-starter:asset-1.0",
"name": "Asset1",
@@ -162,7 +160,9 @@ def test_ls():
"scene.ma"
)
}
- ]
+ ],
+ "time": "",
+ "author": "mottosso",
},
]
}
@@ -188,4 +188,4 @@ def test_ls_no_shareddir():
os.makedirs(no_shared)
with root(no_shared):
- assert next(pyblish_starter.ls(), None) is None
+ assert next(pipeline.ls(), None) is None