Beispiel #1
0
    def get_boolean_setting(self, section, name):
        """
        Provides access to any setting, including ones in user defined sections, and casts it
        into a boolean.

        Values ``1``, ``yes``, ``true`` and ``on`` are converted to ``True`` while ``0``, ``no``,
        ``false``and ``off`` are converted to false. Case is insensitive.

        :param str section: Name of the section to retrieve the setting from. Do not include the brackets.
        :param str name: Name of the setting under the provided section.

        :returns: Boolean if the value is valid, None if not set.
        :rtype: bool

        :raises DskError: Raised if the value is not one of the accepted values.
        """
        value = self.get_setting(section, name)
        if value is None:
            return None

        if value.lower() in self._boolean_states:
            return self._boolean_states[value.lower()]
        else:
            raise DskError("Invalid value '%s' in '%s' for setting '%s' in "
                           "section '%s': expecting one of '%s'." %
                           (value, self._path, name, section, "', '".join(
                               self._boolean_states.keys())))
Beispiel #2
0
    def get_site_root_name(cls, hostname):
        """
        Returns root name of site

        :param hostname:  hostname as string, e.g. 'https://foo.blah.com'
        :param path_type: Type of path to return.
                            One of ``LocalFileStorageManager.LOGGING``,
                            ``LocalFileStorageManager.CACHE``,
                            ``LocalFileStorageManager.PERSISTENT``, where
                            logging is a path where log- and debug related
                            data should be stored, cache is a location intended
                            for cache data, e.g. data that can be deleted
                          without affecting the state of execution,
                          and persistent is a location intended
                          for data that is meant to be persist.
                          This includes things like settings andpreferences.

        :return: Path as string
        """
        if hostname is None:
            raise DskError(
                "Cannot compute path"
                " for local site specific storage - no dsk host specified!")

        # get site only; https://www.FOO.com:8080 -> www.foo.com
        base_url = urlparse(hostname).netloc.split(":")[0].lower()
        res = cls.PSITE.search(base_url)
        if res:
            base_url = base_url.replace(res.group(0), "")
        return base_url
 def __init__(self, error_message, entity=None):
     """
     :param str error_message: An error message, typically coming from a caught exception.
     :param dict entity: The Shotgun entity which was created, if any.
     """
     self.error_message = error_message
     self.entity = entity
     extra_message = "."
     if self.entity:
         # Mention the created entity in the message by appending something like:
         # , although TankPublishedFile dummy_path.txt (id: 2) was created.
         extra_message = ", although %s %s (id: %d) was created." % (
             self.entity["type"], self.entity["code"], self.entity["id"])
     DskError.__init__(
         self,
         "Unable to complete publishing because of the following error: %s%s"
         % (self.error_message, extra_message))
Beispiel #4
0
def __get_sg_config_data(shotgun_cfg_path, user="******"):
    """
    Returns the shotgun configuration yml parameters given a config file.
    
    The shotgun.yml may look like:

        host: str
        api_script: str
        api_key: str
        http_proxy: str
    
        or may now look like:
    
        <User>:
            host: str
            api_script: str
            api_key: str
            http_proxy: str
    
        <User>:
            host: str
            api_script: str
            api_key: str
            http_proxy: str

    The optional user param refers to the <User> in the shotgun.yml.
    If a user is not found the old style is attempted.    
    
    :param shotgun_cfg_path: path to config file
    :param user: Optional user to pass when a multi-user config is being read 

    :returns: dictionary with key host and optional keys api_script, api_key and http_proxy
    """
    # load the config file
    try:
        file_data = yaml_cache.g_yaml_cache.get(shotgun_cfg_path,
                                                deepcopy_data=False) or {}
    except Exception as error:
        raise DskError("Cannot load config file '%s'. Error: %s" %
                       (shotgun_cfg_path, error))
    print("CONNECTION" * 100)
    return _parse_config_data(file_data, user, shotgun_cfg_path)
Beispiel #5
0
def __get_sg_config_data_with_script_user(shotgun_cfg_path, user="******"):
    """
    Returns the Shotgun configuration yml parameters given a config file, just like
    __get_sg_config_data, but the script user is expected to be present or an exception will be
    thrown.

    :param shotgun_cfg_path: path to config file
    :param user: Optional user to pass when a multi-user config is being read

    :raises DskError: Raised if the script user is not configured.

    :returns: dictionary with mandatory keys host, api_script, api_key and optionally http_proxy
    """
    config_data = __get_sg_config_data(shotgun_cfg_path, user)
    # If the user is configured, we're happy.
    if config_data.get("api_script") and config_data.get("api_key"):
        return config_data
    else:
        raise DskError("Missing required script user in config '%s'" %
                       shotgun_cfg_path)
Beispiel #6
0
    def get_integer_setting(self, section, name):
        """
        Provides access to any setting, including ones in user defined sections, and casts it
        into an integer.

        :param str section: Name of the section to retrieve the setting from. Do not include the brackets.
        :param str name: Name of the setting under the provided section.

        :returns: Boolean if the value is valid, None if not set.
        :rtype: bool

        :raises DskError: Raised if the value is not one of the accepted values.
        """
        value = self.get_setting(section, name)
        if value is None:
            return None

        try:
            return int(value)
        except ValueError:
            raise DskError(
                "Invalid value '%s' in '%s' for setting '%s' in section '%s': expecting integer."
                % (value, self._path, name, section))
Beispiel #7
0
def download_url(sg, url, location, use_url_extension=False):
    """
    Convenience method that downloads a file from a given url.
    This method will take into account any proxy settings which have
    been defined in the Shotgun connection parameters.

    In some cases, the target content of the url is not known beforehand.
    For example, the url ``https://my-site.shotgunstudio.com/thumbnail/full/Asset/1227``
    may redirect into ``https://some-site/path/to/a/thumbnail.png``. In
    such cases, you can set the optional use_url_extension parameter to True - this
    will cause the method to append the file extension of the resolved url to
    the filename passed in via the location parameter. So for the urls given
    above, you would get the following results:

    - location="/path/to/file" and use_url_extension=False would return "/path/to/file"
    - location="/path/to/file" and use_url_extension=True would return "/path/to/file.png"

    :param sg: Shotgun API instance to get proxy connection settings from
    :param url: url to download
    :param location: path on disk where the payload should be written.
                     this path needs to exists and the current user needs
                     to have write permissions
    :param bool use_url_extension: Optionally append the file extension of the
                                   resolved URL's path to the input ``location``
                                   to construct the full path name to the downloaded
                                   contents. The newly constructed full path name
                                   will be returned.

    :returns: Full filepath to the downloaded file. This may have been altered from
              the input ``location`` if ``use_url_extension`` is True and a file extension
              could be determined from the resolved url.
    :raises: :class:`DskError` on failure.
    """
    # We only need to set the auth cookie for downloads from Shotgun server,
    # input URLs like: https://my-site.shotgunstudio.com/thumbnail/full/Asset/1227
    if sg.config.server in url:
        # this method also handles proxy server settings from the shotgun API
        __setup_sg_auth_and_proxy(sg)
    elif sg.config.proxy_handler:
        # These input URLs have generally already been authenticated and are
        # in the form: https://sg-media-staging-usor-01.s3.amazonaws.com/9d93f...
        # %3D&response-content-disposition=filename%3D%22jackpot_icon.png%22.
        # Grab proxy server settings from the shotgun API
        opener = urllib2.build_opener(sg.config.proxy_handler)

        urllib2.install_opener(opener)

    # inherit the timeout value from the sg API
    timeout = sg.config.timeout_secs

    # download the given url
    try:
        request = urllib2.Request(url)
        if timeout and sys.version_info >= (2, 6):
            # timeout parameter only available in python 2.6+
            response = urllib2.urlopen(request, timeout=timeout)
        else:
            # use system default
            response = urllib2.urlopen(request)

        if use_url_extension:
            # Make sure the disk location has the same extension as the url path.
            # Would be nice to see this functionality moved to back into Shotgun
            # API and removed from here.
            url_ext = os.path.splitext(urlparse(response.geturl()).path)[-1]
            if url_ext:
                location = "%s%s" % (location, url_ext)

        f = open(location, "wb")
        try:
            f.write(response.read())
        finally:
            f.close()
    except Exception as e:
        raise DskError(
            "Could not download contents of url '%s'. Error reported: %s" %
            (url, e))

    return location
Beispiel #8
0
def create_sg_connection(user="******"):
    """
    Creates a standard sgtk shotgun connection.

    Note! This method returns *a brand new sg API instance*. It is slow.
    Always consider using tk.shotgun and if you don't have a tk instance,
    consider using get_sg_connection().

    Whenever a Shotgun API instance is created, it pings the server to check that
    it is running the right versions etc. This is slow and inefficient and means that
    there will be a delay every time create_sg_connection is called.

    :param user: Optional shotgun config user to use when connecting to shotgun,
                 as defined in shotgun.yml. This is a deprecated flag and should not
                 be used.
    :returns: SG API instance
    """

    # Avoids cyclic imports.
    from ... import api
    sg_user = api.get_authenticated_user()

    # If there is no user, that's probably because we're running in an old script that doesn't use
    # the authenticated user concept. In that case, we'll do what we've always been doing in the
    # past, which is read shotgun.yml and expect there to be a script user.
    if sg_user is None:
        log.debug(
            "This tk session has no associated authenticated user. Falling back to "
            "creating a shotgun API instance based on script based credentials in the "
            "shotgun.yml configuration file.")

        # try to find the shotgun.yml path
        try:
            config_file_path = __get_sg_config()
        except DskError as e:
            log.error(
                "Trying to create a shotgun connection but this tk session does not have "
                "an associated authenticated user. Therefore attempted to fall back on "
                "a legacy authentication method where script based credentials are "
                "located in a file relative to the location of the core API code. This "
                "lookup in turn failed. No credentials can be determined and no connection "
                "to Shotgun can be made. Details: %s" % e)
            raise DskError(
                "Cannot connect to Shotgun - this tk session does not have "
                "an associated user and attempts to determine a valid shotgun "
                "via legacy configuration files failed. Details: %s" % e)

        log.debug("Creating shotgun connection based on details in %s" %
                  config_file_path)
        config_data = __get_sg_config_data_with_script_user(
            config_file_path, user)

        # Credentials were passed in, so let's run the legacy authentication
        # mechanism for script user.
        api_handle = shotgun_api3.Shotgun(
            config_data["host"],
            script_name=config_data["api_script"],
            api_key=config_data["api_key"],
            http_proxy=config_data.get("http_proxy"),
            connect=False)

    else:
        # Otherwise use the authenticated user to create the connection.
        log.debug("Creating shotgun connection from %r..." % sg_user)
        api_handle = sg_user.create_sg_connection()

    # bolt on our custom user agent manager so that we can
    # send basic version metrics back via http headers.
    api_handle.tk_user_agent_handler = ToolkitUserAgentHandler(api_handle)

    return api_handle
Beispiel #9
0
 def _raise_missing_key(key):
     raise DskError(
         "Missing required field '%s' in config '%s' for script user authentication."
         % (key, shotgun_cfg_path))
Beispiel #10
0
def register_publish(tk, context, path, name, version_number, **kwargs):
    """
    Creates a Published File in Shotgun.

    **Introduction**

    The publish will be associated with the current context and point
    at the given file. The method will attempt to add the publish to
    Shotgun as a local file link, and failing that it will generate
    a ``file://`` url to represent the path.

    In addition to the path, a version number and a name needs to be provided.
    The version number should reflect the iteration or revision of the publish
    and will be used to populate the number field of the publish that is created
    in Shotgun. The name should represent the name of the item, without any version
    number. This is used to group together publishes in Shotgun and various
    integrations.

    If the path matches any local storage roots defined by the toolkit project,
    it will be uploaded as a local file link to Shotgun. If not matching roots
    are found, the method will retrieve the list of local storages from Shotgun
    and try to locate a suitable storage. Failing that, it will fall back on a
    register the path as a ``file://`` url. For more information on
    this resolution logic, see our
    `Admin Guide <https://support.shotgunsoftware.com/hc/en-us/articles/115000067493#Configuring%20published%20file%20path%20resolution>`_.

    .. note:: Shotgun follows a convention where the name passed to the register publish method is used
              to control how things are grouped together. When Shotgun and Toolkit groups things together,
              things are typically grouped first by project/entity/task and then by publish name and version.

              If you create three publishes in Shotgun, all having the name 'foreground.ma' and version numbers
              1, 2 and 3, Shotgun will assume that these are three revisions of the same content and will
              group them together in a group called 'foreground.ma'.

              We recommend a convention where the ``name`` parameter reflects the filename passed in via
              the ``file_path`` parameter, but with the version number removed. For example:

              - ``file_path: /tmp/layout.v027.ma, name: layout.ma, version_number: 27``
              - ``file_path: /tmp/foreground_v002.%04d.exr, name: foreground.exr, version_number: 2``

    .. note:: When publishing file sequences, the method will try to normalize your path based on the
              current template configuration. For example, if you supply the path ``render.$F4.dpx``,
              it will translated to ``render.%04d.dpx`` automatically, assuming there is a matching
              template defined. If you are not using templates or publishing files that do not match
              any configured templates, always provide sequences on a ``%0xd`` or
              ``%xd`` `printf <https://en.wikipedia.org/wiki/Printf_format_string>`_ style
              pattern.

    **Examples**

    The example below shows a basic publish. In addition to the required parameters, it is
    recommended to supply at least a comment and a Publish Type::

        >>> file_path = '/studio/demo_project/sequences/Sequence-1/shot_010/Anm/publish/layout.v001.ma'
        >>> name = 'layout.ma'
        >>> version_number = 1
        >>>
        >>> sgtk.util.register_publish(
            tk,
            context,
            file_path,
            name,
            version_number,
            comment = 'Initial layout composition.',
            published_file_type = 'Maya Scene'
        )

        {'code': 'layout.v001.ma',
         'created_by': {'id': 40, 'name': 'John Smith', 'type': 'HumanUser'},
         'description': 'Initial layout composition.',
         'entity': {'id': 2, 'name': 'shot_010', 'type': 'Shot'},
         'id': 2,
         'published_file_type': {'id': 134, 'type': 'PublishedFileType'},
         'name': 'layout.ma',
         'path': {'content_type': None,
          'link_type': 'local',
          'local_path': '/studio/demo_project/sequences/Sequence-1/shot_010/Anm/publish/layout.v001.ma',
          'local_path_linux': '/studio/demo_project/sequences/Sequence-1/shot_010/Anm/publish/layout.v001.ma',
          'local_path_mac': '/studio/demo_project/sequences/Sequence-1/shot_010/Anm/publish/layout.v001.ma',
          'local_path_windows': 'c:\\studio\\demo_project\\sequences\\Sequence-1\\shot_010\\Anm\\publish\\layout.v001.ma',
          'local_storage': {'id': 1, 'name': 'primary', 'type': 'LocalStorage'},
          'name': 'layout.v001.ma',
          'url': 'file:///studio/demo_project/sequences/Sequence-1/shot_010/Anm/publish/layout.v001.ma'},
         'path_cache': 'demo_project/sequences/Sequence-1/shot_010/Anm/publish/layout.v001.ma',
         'project': {'id': 4, 'name': 'Demo Project', 'type': 'Project'},
         'published_file_type': {'id': 12, 'name': 'Layout Scene', 'type': 'PublishedFileType'},
         'task': None,
         'type': 'PublishedFile',
         'version_number': 1}

    When using the ``dry_run`` option, the returned data will look something like this::

        >>> file_path = '/studio/demo_project/sequences/Sequence-1/shot_010/Anm/publish/layout.v001.ma'
        >>> name = 'layout'
        >>> version_number = 1
        >>>
        >>> sgtk.util.register_publish(
            tk,
            context,
            file_path,
            name,
            version_number,
            comment='Initial layout composition.',
            published_file_type='Layout Scene'
            dry_run=True
        )

        {'code': 'layout.v001.ma',
         'description': 'Initial layout composition.',
         'entity': {'id': 2, 'name': 'shot_010', 'type': 'Shot'},
         'path': {'local_path': '/studio/demo_project/sequences/Sequence-1/shot_010/Anm/publish/layout.v001.ma'},
         'project': {'id': 4, 'name': 'Demo Project', 'type': 'Project'},
         'task': None,
         'type': 'PublishedFile',
         'version_number': 1}

    Be aware that the data may be different if the ``before_register_publish``
    hook has been overridden.

    **Parameters**

    :param tk: :class:`~sgtk.Sgtk` instance
    :param context: A :class:`~sgtk.Context` to associate with the publish. This will
                    populate the ``task`` and ``entity`` link in Shotgun.
    :param path: The path to the file or sequence we want to publish. If the
                 path is a sequence path it will be abstracted so that
                 any sequence keys are replaced with their default values.
    :param name: A name, without version number, which helps distinguish
               this publish from other publishes. This is typically
               used for grouping inside of Shotgun so that all the
               versions of the same "file" can be grouped into a cluster.
               For example, for a Maya publish, where we track only
               the scene name, the name would simply be that: the scene
               name. For something like a render, it could be the scene
               name, the name of the AOV and the name of the render layer.
    :param version_number: The version number of the item we are publishing.


    In addition to the above, the following optional arguments exist:

        - ``task`` - A shotgun entity dictionary with keys ``id`` and ``type`` (where type should always be ``Task``).
          This value will be used to populate the task field for the created Shotgun publish record.
          If no value is specified, the task will be determined based on the context parameter.

        - ``comment`` - A string containing a description of what is being published.

        - ``thumbnail_path`` - A path to a thumbnail (png or jpeg) which will be uploaded to shotgun
          and associated with the publish.

        - ``dependency_paths`` - A list of file system paths that should be attempted to be registered
          as dependencies. Files in this listing that do not appear as publishes in shotgun will be ignored.

        - ``dependency_ids`` - A list of publish ids which should be registered as dependencies.

        - ``published_file_type`` - A publish type in the form of a string. If the publish type does not
          already exist in Shotgun, it will be created.

        - ``update_entity_thumbnail`` - Push thumbnail up to the associated entity

        - ``update_task_thumbnail`` - Push thumbnail up to the associated task

        - ``created_by`` - Override for the user that will be marked as creating the publish.  This should
          be in the form of shotgun entity, e.g. {"type":"HumanUser", "id":7}. If not set, the user will
          be determined using :meth:`sgtk.util.get_current_user`.

        - ``created_at`` - Override for the date the publish is created at.  This should be a python
          datetime object

        - ``version_entity`` - The Shotgun review version that the publish should be linked to. This
          should be a dictionary of keys ``id`` and ``type`` (where type should always be ``Version``).
          This parameter is useful for workflows where a Shotgun Version has already been created for review
          purposes and you want to associate the publish created by this method.

          Note: For workflows where you have an existing review version and want to create a series of associated
          publishes, you may want to extract a :class:`~sgtk.Context` from the Version entity and pass that
          to the :meth:`register_publish` method in order to ensure consistency in how objects are associated
          in Shotgun.

        - ``sg_fields`` - Some additional Shotgun fields as a dict (e.g. ``{'sg_custom_field': 'hello'}``)

        - ``dry_run`` - Boolean. If set, do not actually create a database entry. Return the
          dictionary of data that would be supplied to Shotgun to create the PublishedFile entity.

    :raises: :class:`ShotgunPublishError` on failure.
    :returns: The created entity dictionary.
    """
    log.debug(
        "Publish: Begin register publish for context {0} and path {1}".format(context, path)
    )
    entity = None
    try:
        # get the task from the optional args, fall back on context task if not set
        task = kwargs.get("task")
        if task is None:
            task = context.task

        thumbnail_path = kwargs.get("thumbnail_path")
        comment = kwargs.get("comment")
        dependency_paths = kwargs.get('dependency_paths', [])
        dependency_ids = kwargs.get('dependency_ids', [])
        published_file_type = kwargs.get("published_file_type")
        if not published_file_type:
            # check for legacy name:
            published_file_type = kwargs.get("tank_type")
        update_entity_thumbnail = kwargs.get("update_entity_thumbnail", False)
        update_task_thumbnail = kwargs.get("update_task_thumbnail", False)
        created_by_user = kwargs.get("created_by")
        created_at = kwargs.get("created_at")
        version_entity = kwargs.get("version_entity")
        sg_fields = kwargs.get("sg_fields", {})
        dry_run = kwargs.get("dry_run", False)

        published_file_entity_type = get_published_file_entity_type(tk)

        log.debug("Publish: Resolving the published file type")
        sg_published_file_type = None
        # query shotgun for the published_file_type
        if published_file_type:
            if not isinstance(published_file_type, basestring):
                raise DskError("published_file_type must be a string")

            if published_file_entity_type == "PublishedFile":
                filters = [["code", "is", published_file_type]]
                sg_published_file_type = tk.shotgun.find_one('PublishedFileType', filters=filters)

                if not sg_published_file_type:
                    # create a published file type on the fly
                    sg_published_file_type = tk.shotgun.create("PublishedFileType", {"code": published_file_type})
            else:  # == TankPublishedFile
                filters = [["code", "is", published_file_type], ["project", "is", context.project]]
                sg_published_file_type = tk.shotgun.find_one('TankType', filters=filters)

                if not sg_published_file_type:
                    # create a tank type on the fly
                    sg_published_file_type = tk.shotgun.create("TankType", {"code": published_file_type,
                                                                            "project": context.project})

        # create the publish
        log.debug("Publish: Creating publish in Shotgun")
        entity = _create_published_file(tk,
                                        context,
                                        path,
                                        name,
                                        version_number,
                                        task,
                                        comment,
                                        sg_published_file_type,
                                        created_by_user,
                                        created_at,
                                        version_entity,
                                        sg_fields,
                                        dry_run=dry_run)

        if not dry_run:
            # upload thumbnails
            log.debug("Publish: Uploading thumbnails")
            if thumbnail_path and os.path.exists(thumbnail_path):

                # publish
                tk.shotgun.upload_thumbnail(published_file_entity_type, entity["id"], thumbnail_path)

                # entity
                if update_entity_thumbnail == True and context.entity is not None:
                    tk.shotgun.upload_thumbnail(context.entity["type"],
                                                context.entity["id"],
                                                thumbnail_path)

                # task
                if update_task_thumbnail == True and task is not None:
                    tk.shotgun.upload_thumbnail("Task", task["id"], thumbnail_path)

            else:
                # no thumbnail found - instead use the default one
                this_folder = os.path.abspath(os.path.dirname(__file__))
                no_thumb = os.path.join(this_folder, os.path.pardir, "resources", "no_preview.jpg")
                tk.shotgun.upload_thumbnail(published_file_entity_type, entity.get("id"), no_thumb)

            # register dependencies
            log.debug("Publish: Register dependencies")
            _create_dependencies(tk, entity, dependency_paths, dependency_ids)
            log.debug("Publish: Complete")

        return entity
    except Exception as e:
        # Log the exception so the original traceback is available
        log.exception(e)
        if "[Attachment.local_storage] does not exist" in str(e):
            raise ShotgunPublishError(
                "Local File Linking seems to be turned off. "
                "Turn it on on your Site Preferences Page.",
                entity
            )
        else:
            # Raise our own exception with the original message and the created entity,
            # if any
            raise ShotgunPublishError(
                error_message="%s" % e,
                entity=entity
            )
Beispiel #11
0
def _create_published_file(tk, context, path, name, version_number, task, comment, published_file_type,
                           created_by_user, created_at, version_entity, sg_fields=None, dry_run=False):
    """
    Creates a publish entity in shotgun given some standard fields.

    :param tk: :class:`~sgtk.Sgtk` instance
    :param context: A :class:`~sgtk.Context` to associate with the publish. This will
                    populate the ``task`` and ``entity`` link in Shotgun.
    :param path: The path to the file or sequence we want to publish. If the
                 path is a sequence path it will be abstracted so that
                 any sequence keys are replaced with their default values.
    :param name: A name, without version number, which helps distinguish
               this publish from other publishes. This is typically
               used for grouping inside of Shotgun so that all the
               versions of the same "file" can be grouped into a cluster.
               For example, for a Maya publish, where we track only
               the scene name, the name would simply be that: the scene
               name. For something like a render, it could be the scene
               name, the name of the AOV and the name of the render layer.
    :param version_number: The version number of the item we are publishing.
    :param task: Shotgun Task dictionary to associate with publish or ``None``
    :param comment: Comments string to associate with publish
    :param published_file_type: Shotgun publish type dictionary to
                associate with publish
    :param created_by_user: User entity to associate with publish or ``None``
                if current user (via :meth:`sgtk.util.get_current_user`)
                should be used.
    :param created_at: Timestamp to associate with publish or None for default.
    :param version_entity: Version dictionary to associate with publish or ``None``.
    :param sg_fields: Dictionary of additional data to add to publish.
    :param dry_run: Don't actually create the published file entry. Simply
                    return the data dictionary that would be supplied.

    :returns: The result of the shotgun API create method.
    """

    data = {
        "description": comment,
        "name": name,
        "task": task,
        "version_number": version_number,
        }

    # we set the optional additional fields first so we don't allow overwriting the standard parameters
    if sg_fields is None:
        sg_fields = {}
    data.update(sg_fields)

    if created_by_user:
        data["created_by"] = created_by_user
    else:
        # use current user
        sg_user = login.get_current_user(tk)
        if sg_user:
            data["created_by"] = sg_user

    if created_at:
        data["created_at"] = created_at

    published_file_entity_type = get_published_file_entity_type(tk)

    if published_file_type:
        if published_file_entity_type == "PublishedFile":
            data["published_file_type"] = published_file_type
        else:
            # using legacy type TankPublishedFile
            data["tank_type"] = published_file_type

    if version_entity:
        data["version"] = version_entity


    # Determine the value of the link field based on the given context
    if context.project is None:
        # when running toolkit as a standalone plugin, the context may be
        # empty and not contain a project. Publishes are project entities
        # in Shotgun, so we cannot proceed without a project.
        raise DskError("Your context needs to at least have a project set in order to publish.")

    elif context.entity is None:
        # If the context does not have an entity, link it up to the project.
        # This happens for project specific workflows such as editorial
        # workflows, ingest and when running zero config toolkit plugins in
        # a generic project mode.
        data["entity"] = context.project

    else:
        data["entity"] = context.entity

    # set the associated project
    data["project"] = context.project

    # Check if path is a url or a straight file path.  Path
    # is assumed to be a url if it has a scheme:
    #
    #     scheme://netloc/path
    #
    path_is_url = False
    res = urlparse(path)
    if res.scheme:
        # handle Windows drive letters - note this adds a limitation
        # but one that is not likely to be a problem as single-character
        # schemes are unlikely!
        if len(res.scheme) > 1 or not res.scheme.isalpha():
            path_is_url = True

    # naming and path logic is different depending on url
    if path_is_url:

        # extract name from url:
        #
        # scheme://hostname.com/path/to/file.ext -> file.ext
        # scheme://hostname.com -> hostname.com
        if res.path:
            # scheme://hostname.com/path/to/file.ext -> file.ext
            data["code"] = res.path.split("/")[-1]
        else:
            # scheme://hostname.com -> hostname.com
            data["code"] = res.netloc

        # make sure that the url is escaped property, otherwise
        # shotgun might not accept it.
        #
        # for quoting logic, see bugfix here:
        # http://svn.python.org/view/python/trunk/Lib/urllib.py?r1=71780&r2=71779&pathrev=71780
        #
        # note: by applying a safe pattern like this, we guarantee that already quoted paths
        #       are not touched, e.g. quote('foo bar') == quote('foo%20bar')
        data["path"] = {
            "url": quote(path, safe="%/:=&?~#+!$,;'@()*[]"),
            "name": data["code"]  # same as publish name
        }

    else:

        # normalize the path to native slashes
        norm_path = ShotgunPath.normalize(path)
        if norm_path != path:
            log.debug("Normalized input path '%s' -> '%s'" % (path, norm_path))
            path = norm_path

        # convert the abstract fields to their defaults
        path = _translate_abstract_fields(tk, path)

        # name of publish is the filename
        data["code"] = os.path.basename(path)

        # Make path platform agnostic and determine if it belongs
        # to a storage that is associated with this toolkit config.
        root_name, path_cache = _calc_path_cache(tk, path)

        if path_cache:
            # there is a toolkit storage mapping defined for this storage
            log.debug(
                "The path '%s' is associated with config root '%s'." % (path, root_name)
            )

            # check if the shotgun server supports the storage and relative_path parameters which
            # allows us to specify exactly which storage to bind a publish to rather than relying on
            # Shotgun to compute this.
            supports_specific_storage_syntax = (
                hasattr(tk.shotgun, "server_caps") and
                tk.shotgun.server_caps.version and
                tk.shotgun.server_caps.version >= (7, 0, 1)
            )

            if supports_specific_storage_syntax:

                # get corresponding SG local storage for the matching root name
                storage = tk.pipeline_configuration.get_local_storage_for_root(root_name)

                if storage is None:
                    # there is no storage in Shotgun that matches the one toolkit expects.
                    # this *may* be ok because there may be another storage in Shotgun that
                    # magically picks up the publishes and associates with them. In this case,
                    # issue a warning and fall back on the server-side functionality
                    log.warning(
                        "Could not find the expected storage for required root "
                        "'%s' in Shotgun to associate publish '%s' with. "
                        "Falling back to Shotgun's built-in storage resolution "
                        "logic. It is recommended that you explicitly map a "
                        "local storage to required root '%s'." %
                        (root_name, path, root_name))
                    data["path"] = {"local_path": path}

                else:
                    data["path"] = {"relative_path": path_cache, "local_storage": storage}

            else:
                # use previous syntax where we pass the whole path to Shotgun
                # and shotgun will do the storage/relative path split server side.
                # This operation may do unexpected things if you have multiple
                # storages that are identical or overlapping
                data["path"] = {"local_path": path}

            # fill in the path cache field which is used for filtering in Shotgun
            # (because SG does not support
            data["path_cache"] = path_cache

        else:

            # path does not map to any configured root - fall back gracefully:
            # 1. look for storages in Shotgun and see if we can create a local path
            # 2. failing that, just register the entry as a file:// resource.
            log.debug("Path '%s' does not have an associated config root." % path)
            log.debug("Will check shotgun local storages to see if there is a match.")

            matching_local_storage = False
            for storage in get_cached_local_storages(tk):
                local_storage_path = ShotgunPath.from_shotgun_dict(storage).current_os
                # assume case preserving file systems rather than case sensitive
                if local_storage_path and path.lower().startswith(local_storage_path.lower()):
                    log.debug("Path matches Shotgun local storage '%s'" % storage["code"])
                    matching_local_storage = True
                    break

            if matching_local_storage:
                # there is a local storage matching this path
                # so use that when publishing
                data["path"] = {"local_path": path}

            else:
                # no local storage defined so publish as a file:// url
                log.debug(
                    "No local storage matching path '%s' - path will be "
                    "registered as a file:// url." % (path, )
                )

                # (see http://stackoverflow.com/questions/11687478/convert-a-filename-to-a-file-url)
                file_url = urlparse.urljoin("file:", pathname2url(path))
                log.debug("Converting '%s' -> '%s'" % (path, file_url))
                data["path"] = {
                    "url": file_url,
                    "name": data["code"]  # same as publish name
                }


    # now call out to hook just before publishing
    data = tk.execute_core_hook(dsk_constants.PUBLISH_HOOK_NAME, shotgun_data=data, context=context)

    if dry_run:
        # add the publish type to be as consistent as possible
        data["type"] = published_file_entity_type
        log.debug("Dry run. Simply returning the data that would be sent to SG: %s" % pprint.pformat(data))
        return data
    else:
        log.debug("Registering publish in Shotgun: %s" % pprint.pformat(data))
        return tk.shotgun.create(published_file_entity_type, data)