コード例 #1
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
class PathExpansionTest(unittest.TestCase):
    def setUp(self):
        self.env = {
            "HOME": "/users/joebloggs",
            "SHOT": "/metropolis/shot01",
            "DEPT": "texturing",
        }

    def test_posix_tilde_input(self):
        with mock.patch.dict("os.environ", self.env):
            self.p = Path("~/a/b/c")
            self.assertEqual(self.p.posix_path(), "/users/joebloggs/a/b/c")

    def test_posix_var_input(self):
        with mock.patch.dict("os.environ", self.env):
            self.p = Path("$SHOT/a/b/c")
            self.assertEqual(self.p.posix_path(), "/metropolis/shot01/a/b/c")

    def test_posix_two_var_input(self):
        with mock.patch.dict("os.environ", self.env):
            self.p = Path("$SHOT/a/b/$DEPT/c")
            self.assertEqual(self.p.posix_path(),
                             "/metropolis/shot01/a/b/texturing/c")

    def test_windows_var_input(self):
        with mock.patch.dict("os.environ", self.env):
            self.p = Path("$HOME\\a\\b\\c")
            self.assertEqual(self.p.windows_path(),
                             "\\users\\joebloggs\\a\\b\\c")
            self.assertEqual(self.p.posix_path(), "/users/joebloggs/a/b/c")
コード例 #2
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
class SpecifyDriveLetterUse(unittest.TestCase):
    def test_remove_from_path(self):
        self.p = Path("C:\\a\\b\\c")
        self.assertEqual(self.p.posix_path(with_drive=False), "/a/b/c")
        self.assertEqual(self.p.windows_path(with_drive=False), "\\a\\b\\c")

    def test_remove_from_root_path(self):
        self.p = Path("C:\\")
        self.assertEqual(self.p.posix_path(with_drive=False), "/")
        self.assertEqual(self.p.windows_path(with_drive=False), "\\")
コード例 #3
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
class RootPath(unittest.TestCase):
    def test_root_path(self):
        self.p = Path("/")
        self.assertEqual(self.p.posix_path(), "/")
        self.assertEqual(self.p.windows_path(), "\\")

    def test_drive_letter_root_path(self):
        self.p = Path("C:\\")
        self.assertEqual(self.p.posix_path(), "C:/")
        self.assertEqual(self.p.windows_path(), "C:\\")
コード例 #4
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
class WindowsMixedPathTest(unittest.TestCase):
    def test_abs_in_posix_path_out(self):
        self.p = Path("\\a\\b\\c/d/e")
        self.assertEqual(self.p.posix_path(), "/a/b/c/d/e")

    def test_abs_in_windows_path_out(self):
        self.p = Path("\\a\\b\\c/d/e")
        self.assertEqual(self.p.windows_path(), "\\a\\b\\c\\d\\e")

    def test_letter_abs_in_posix_path_out(self):
        self.p = Path("C:\\a\\b\\c/d/e")
        self.assertEqual(self.p.posix_path(), "C:/a/b/c/d/e")

    def test_letter_abs_in_windows_path_out(self):
        self.p = Path("C:\\a\\b\\c/d/e")
        self.assertEqual(self.p.windows_path(), "C:\\a\\b\\c\\d\\e")
コード例 #5
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
class AbsPosixPathTest(unittest.TestCase):
    def setUp(self):
        self.p = Path("/a/b/c")

    def test_posix_path_out(self):
        self.assertEqual(self.p.posix_path(), "/a/b/c")

    def test_win_path_out(self):
        self.assertEqual(self.p.windows_path(), "\\a\\b\\c")
コード例 #6
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
class PathContextExpansionTest(unittest.TestCase):
    def setUp(self):

        self.env = {
            "HOME": "/users/joebloggs",
            "SHOT": "/metropolis/shot01",
            "DEPT": "texturing",
        }

        self.context = {
            "HOME": "/users/janedoe",
            "FOO": "fooval",
            "BAR_FLY1_": "bar_fly1_val",
            "ROOT_DIR": "/some/root",
        }

    def test_path_replaces_context(self):
        self.p = Path("$ROOT_DIR/thefile.jpg", context=self.context)
        self.assertEqual(self.p.posix_path(), "/some/root/thefile.jpg")

    def test_path_replaces_multiple_context(self):
        self.p = Path("$ROOT_DIR/$BAR_FLY1_/thefile.jpg", context=self.context)
        self.assertEqual(self.p.posix_path(),
                         "/some/root/bar_fly1_val/thefile.jpg")

    def test_path_context_overrides_env(self):
        self.p = Path("$HOME/thefile.jpg", context=self.context)
        self.assertEqual(self.p.posix_path(), "/users/janedoe/thefile.jpg")

    def test_path_leave_unknown_variable_in_tact(self):
        self.p = Path("$ROOT_DIR/$BAR_FLY1_/$FOO/thefile.$F.jpg",
                      context=self.context)
        self.assertEqual(self.p.posix_path(),
                         "/some/root/bar_fly1_val/fooval/thefile.$F.jpg")

    def test_relative_path_var_fails(self):
        with self.assertRaises(ValueError):
            self.p = Path("$FOO/a/b/c", context=self.context)
コード例 #7
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
class AbsWindowsPathTest(unittest.TestCase):
    def setUp(self):
        self.p = Path("C:\\a\\b\\c")

    def test_posix_path_out(self):
        self.assertEqual(self.p.posix_path(), "C:/a/b/c")

    def test_win_path_out(self):
        self.assertEqual(self.p.windows_path(), "C:\\a\\b\\c")

    # consider just testing on both platforms
    def test_os_path_out(self):
        with mock.patch("os.name", "posix"):
            self.assertEqual(self.p.os_path(), "C:/a/b/c")
        with mock.patch("os.name", "nt"):
            self.assertEqual(self.p.os_path(), "C:\\a\\b\\c")
コード例 #8
0
class Submission(object):
    """
    Submission holds all data needed for a submission.

    It has potentially many Jobs, and those Jobs each have many Tasks. A
    Submission can provide the correct args to send to Conductor, or it can be
    used to create a dry run to show the user what will happen.

    A Submission also sets a list of tokens that the user can access as <angle
    bracket> tokens in order to build strings in the UI such as commands, job
    title, and (soon to be added) metadata.
    """
    def __init__(self, obj):
        """
        Collect data from the Clarisse UI.

        Collect attribute values that are common to all jobs, then call
        _set_tokens(). After _set_tokens has been called, the Submission level
        token variables are valid and calls to evaluate expressions will
        correctly resolve where those tokens have been used.
        """
        self.node = obj

        if self.node.is_kindof("ConductorJob"):
            self.nodes = [obj]
        else:
            raise NotImplementedError

        self.localize_before_ship = self.node.get_attribute(
            "localize_contexts").get_bool()
        self.project_filename = ix.application.get_current_project_filename()
        self.timestamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
        self.timestamp_render_package = self.node.get_attribute(
            "timestamp_render_package").get_bool()

        self.tmpdir = Path(
            os.path.join(
                ix.application.get_factory().get_vars().get(
                    "CTEMP").get_string(),
                "conductor",
            ))
        self.render_package_path = self._get_render_package_path()
        self.should_delete_render_package = self.node.get_attribute(
            "clean_up_render_package").get_bool()

        self.local_upload = self.node.get_attribute("local_upload").get_bool()
        self.force_upload = self.node.get_attribute("force_upload").get_bool()
        self.upload_only = self.node.get_attribute("upload_only").get_bool()
        self.project = self._get_project()
        self.notifications = self._get_notifications()
        self.tokens = self._set_tokens()

        self.jobs = []
        for node in self.nodes:
            job = Job(node, self.tokens, self.render_package_path)
            self.jobs.append(job)

    def _get_project(self):
        """Get the project from the attr.

        Get its ID in case the current project is no longer in the list
        of projects at conductor, throw an error.
        """

        projects = ConductorDataBlock().projects()
        project_att = self.node.get_attribute("conductor_project_name")
        label = project_att.get_applied_preset_label()
        try:
            found = next(p for p in projects if str(p["name"]) == label)
        except StopIteration:
            ix.log_error(
                'Cannot find project "{}" at Conductor.'.format(label))

        return {"id": found["id"], "name": str(found["name"])}

    def _get_notifications(self):
        """Get notification prefs."""
        if not self.node.get_attribute("notify").get_bool():
            return None

        emails = self.node.get_attribute("email_addresses").get_string()
        return [email.strip() for email in emails.split(",") if email.strip()]

    def _set_tokens(self):
        """Env tokens are variables to help the user build expressions.

        The user interface has fields for strings such as job title,
        task command. The user can use these tokens with <angle brackets> to build those strings. Tokens at the Submission
        level are also available in Job level fields, and likewise
        tokens at the Job level are available in Task level fields.
        """
        tokens = {}

        pdir_val = ix.application.get_factory().get_vars().get(
            "PDIR").get_string()

        tokens["ct_pdir"] = '"{}"'.format(
            Path(pdir_val).posix_path(with_drive=False))

        tokens["ct_temp_dir"] = "{}".format(
            self.tmpdir.posix_path(with_drive=False))
        tokens["ct_timestamp"] = self.timestamp
        tokens["ct_submitter"] = self.node.get_name()

        tokens["ct_render_package"] = '"{}"'.format(
            self.render_package_path.posix_path(with_drive=False))

        tokens["ct_project"] = self.project["name"]

        return tokens

    def _get_render_package_path(self):
        """
        Calc the path to the render package.

        The name is not always known until
        preview/submission time because it is based on the filename and
        possibly a timestamp. What this means, practically, is that it won't
        show up in the extra uploads window along with
        other dependencies when the glob or smart-scan button is pushed.
        It will however always show up in the preview window.

        We replace spaces in the filename because of a bug in Clarisse
        https://www.isotropix.com/user/bugtracker/376

        Returns:
            string: path
        """
        current_filename = ix.application.get_current_project_filename()
        path = os.path.splitext(current_filename)[0]

        path = os.path.join(os.path.dirname(path),
                            os.path.basename(path).replace(" ", "_"))

        try:
            if self.timestamp_render_package:
                return Path("{}_ct{}.project".format(path, self.timestamp))
            else:
                return Path("{}_ct.project".format(path))
        except ValueError:
            ix.log_error(
                'Cannot create a submission from this file: "{}". Has it ever been saved?'
                .format(current_filename))

    def get_args(self):
        """
        Prepare the args for submission to conductor.

        Returns:
            list: list of dicts containing submission args per job.
        """

        result = []
        submission_args = {}

        submission_args["local_upload"] = self.local_upload
        submission_args["upload_only"] = self.upload_only
        submission_args["force"] = self.force_upload
        submission_args["project"] = self.project["name"]
        submission_args["notify"] = self.notifications

        for job in self.jobs:
            args = job.get_args(self.upload_only)
            args.update(submission_args)
            result.append(args)
        return result

    def submit(self):
        """
        Submit all jobs.

        Returns:
            list: list of response dictionaries, containing response codes
            and descriptions.
        """

        submission_args = self.get_args()
        self.write_render_package()

        do_submission, submission_args = self.legalize_upload_paths(
            submission_args)
        results = []
        if do_submission:

            for job_args in submission_args:
                try:
                    remote_job = conductor_submit.Submit(job_args)
                    response, response_code = remote_job.main()
                    results.append({
                        "code": response_code,
                        "response": response
                    })
                except BaseException:
                    results.append({
                        "code":
                        "undefined",
                        "response":
                        "".join(traceback.format_exception(*sys.exc_info())),
                    })
            for result in results:
                ix.log_info(result)
        else:
            return [{
                "code": "undefined",
                "response": "Submission cancelled by user"
            }]

        self._after_submit()
        return results

    def write_render_package(self):
        """
        Write a package suitable for rendering.

        A render package is a project file with a special name.
        """

        app = ix.application
        clarisse_window = app.get_event_window()

        self._before_write_package()
        current_filename = app.get_current_project_filename()
        current_window_title = clarisse_window.get_title()

        package_file = self.render_package_path.posix_path()
        with cu.disabled_app():
            success = ix.application.save_project(package_file)
            ix.application.set_current_project_filename(current_filename)
            clarisse_window.set_title(current_window_title)

        self._after_write_package()

        if not success:
            ix.log_error(
                "Failed to export render package {}".format(package_file))

        ix.log_info("Wrote package to {}".format(package_file))
        return package_file

    def legalize_upload_paths(self, submission_args):
        """
        Alert the user of missing files. If the user doesn't want to continue
        with missing files, the result will be False. Otherwise it will be True
        and the potentially adjusted args are returned.

        Args:
            submission_args (list): list of job args.

        Returns:
           tuple (bool, adjusted args):
        """
        missing_files = []

        for job_args in submission_args:
            existing_files = []
            for path in job_args["upload_paths"]:
                if os.path.exists(path):
                    existing_files.append(path)
                else:
                    missing_files.append(path)

            job_args["upload_paths"] = existing_files
        missing_files = sorted(list(set(missing_files)))
        if not missing_files_ui.proceed(missing_files):
            return (False, [])
        return (True, submission_args)

    def _before_write_package(self):
        """
        Prepare to write render package.
        """
        if self.localize_before_ship:
            _localize_contexts()
            _remove_conductor()

        self._prepare_temp_directory()
        self._copy_system_dependencies_to_temp()

    def _prepare_temp_directory(self):
        """
        Make sure the temp directory has a conductor subdirectory.
        """
        tmpdir = self.tmpdir.posix_path()
        try:
            os.makedirs(tmpdir)
        except OSError as ex:
            if not (ex.errno == errno.EEXIST and os.path.isdir(tmpdir)):
                raise

    def _copy_system_dependencies_to_temp(self):
        """
        Copy over all system dependencies to a tmp folder.

        Wrapper scripts, config files etc. The clarisse.cfg file is special. See
        ../clarisse_config.py
        """
        for entry in deps.system_dependencies():
            if os.path.isfile(entry["src"]):
                if entry["src"].endswith(".cfg"):
                    safe_config = ccfg.legalize(entry["src"])
                    with open(entry["dest"], "w") as dest:
                        dest.write(safe_config)
                    ix.log_info("Copy with mods {} to {}".format(
                        entry["src"], entry["dest"]))
                else:
                    ix.log_info("Copy {} to {}".format(entry["src"],
                                                       entry["dest"]))
                    shutil.copy(entry["src"], entry["dest"])

    def _after_submit(self):
        """Clean up, and potentially other post submission actions."""
        self._delete_render_package()

    def _after_write_package(self):
        """
        Runs operations after saving the render package.

        If we did something destructive, like localize contexts, then
        a backup will have been saved and we now reload it. This strategy
        is used because Clarisse's undo is broken when it comes to
        undoing context localization.
        """
        if self.localize_before_ship:
            self._revert_to_saved_scene()

    def _delete_render_package(self):
        """
        Delete the render package from disk if the user wants to.
        """
        if self.should_delete_render_package:
            render_package_file = self.render_package_path.posix_path()
            if os.path.exists(render_package_file):
                os.remove(render_package_file)

    def _revert_to_saved_scene(self):
        """
        If contexts were localized, we will load the saved scene.
        """
        with cu.waiting_cursor():
            with cu.disabled_app():
                ix.application.load_project(self.project_filename)

    @property
    def node_name(self):
        """node_name."""
        return self.node.get_name()

    @property
    def filename(self):
        """filename."""
        return ix.application.get_current_project_filename()

    def has_notifications(self):
        """has_notifications."""
        return bool(self.notifications)

    @property
    def email_addresses(self):
        """email_addresses."""
        if not self.has_notifications():
            return []
        return self.notifications["email"]["addresses"]
コード例 #9
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
 def test_path_collapses_to_root(self):
     p = Path("/a/b/../../")
     self.assertEqual(p.posix_path(), "/")
     self.assertEqual(p.depth, 0)
コード例 #10
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
 def test_path_collapses_many_non_consecutive_mixed_dots(self):
     p = Path("/a/./b/c/../.././d/../././e/f/../g/./")
     self.assertEqual(p.posix_path(), "/a/e/g")
     self.assertEqual(p.depth, 3)
コード例 #11
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
 def test_path_collapses_many_non_consecutive_double_dots(self):
     p = Path("/a/b/c/../../d/../e/f/../g")
     self.assertEqual(p.posix_path(), "/a/e/g")
コード例 #12
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
 def test_path_collapses_many_single_dots(self):
     p = Path("/a/b/./c/././d")
     self.assertEqual(p.posix_path(), "/a/b/c/d")
コード例 #13
0
ファイル: test_gpath.py プロジェクト: wwfxuk/conductor_client
 def test_path_collapses_double_dot(self):
     p = Path("/a/b/../c")
     self.assertEqual(p.posix_path(), "/a/c")