diff --git a/.flake8 b/.flake8 index 02cff1e84..343f01039 100644 --- a/.flake8 +++ b/.flake8 @@ -10,4 +10,4 @@ [flake8] max-line-length = 120 -exclude = shotgun_api3/lib/httplib2/*,shotgun_api3/lib/six.py,tests/httplib2test.py,tests/mock.py \ No newline at end of file +exclude = shotgun_api3/lib/httplib2/*,shotgun_api3/lib/six.py,tests/httplib2test.py,tests/mock.py diff --git a/.gitignore b/.gitignore index 3e6ff329a..02018058b 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,3 @@ build dist shotgun_api3.egg-info /%1 - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..537da22cf --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,49 @@ +# Copyright (c) 2024, Shotgun Software Inc. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# - Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# - Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# - Neither the name of the Shotgun Software Inc nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Styles the code properly + +# Exclude Third Pary components +exclude: "shotgun_api3/lib/.*" + +# List of super useful formatters. +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-ast + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: trailing-whitespace + + - repo: https://github.com/psf/black + rev: 25.1.0 + hooks: + - id: black diff --git a/HISTORY.rst b/HISTORY.rst index 82309527e..9858f61c8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,7 +9,7 @@ v3.8.0 (2024 Feb 7) - Extend the payload optimizations to the ``in`` and ``not_in`` filters and the ``update`` method. -- The payload optimization is now enabled by default. +- The payload optimization is now enabled by default. It can be disabled with the ``SHOTGUN_API_DISABLE_ENTITY_OPTIMIZATION`` environment variable. @@ -57,7 +57,7 @@ v3.4.2 (2024 Feb 6) v3.4.1 (2024 Jan 29) ==================== - Flaky Tests -- Documentation: Fix issue regarding "in" filter prototype +- Documentation: Fix issue regarding "in" filter prototype - Documentation: Travis badge image is no working anymore - Documentation: Add ``user_subscription_read`` and ``user_subscription_create`` methods - Update Python Certifi license block @@ -208,7 +208,7 @@ v3.0.34 (2017 September 18) v3.0.33 (2017 July 18) ====================== -- Raise an exception when uploading an empty file using :meth:`upload`, :meth:`upload_thumbnail` +- Raise an exception when uploading an empty file using :meth:`upload`, :meth:`upload_thumbnail` or :meth:`upload_filmstrip_thumbnail` before calling out to the server. - Multiple enhancements and bugfixes to Mockgun - Added ``nav_search_string()`` and ``nav_search_entity()`` methods as experimental, internal methods for querying SG hierarchy. @@ -258,17 +258,17 @@ v3.0.27 (2016 Feb 18) v3.0.26 (2016 Feb 1) ==================== -- Updating testing framework to use environment variables inconjunction with existing +- Updating testing framework to use environment variables inconjunction with existing ``example_config`` file so that commits and pull requests are automatically run on travis-ci. -- Fix to prevent stripping out case-sensitivity of a URL if the user passes their credentials to +- Fix to prevent stripping out case-sensitivity of a URL if the user passes their credentials to ``config.server`` as an authorization header. v3.0.25 (2016 Jan 12) ===================== -- Add handling for Python versions incompatible with SHA-2 (see `this blog post +- Add handling for Python versions incompatible with SHA-2 (see `this blog post `_). -- Add ``SHOTGUN_FORCE_CERTIFICATE_VALIDATION`` environment variable to prevent disabling certficate +- Add ``SHOTGUN_FORCE_CERTIFICATE_VALIDATION`` environment variable to prevent disabling certficate validation when SHA-2 validation is not available. - Add SSL info to user-agent header. @@ -276,11 +276,11 @@ v3.0.24 (2016 Jan 08) ===================== - Not released. - + v3.0.23 (2015 Oct 26) ===================== -- Fix for `python bug #23371 `_ on Windows loading mimetypes +- Fix for `python bug #23371 `_ on Windows loading mimetypes module (thanks `@patrickwolf `_). - Fix for tests on older versions of python. - Sanitize authentication values before raising error. @@ -288,13 +288,13 @@ v3.0.23 (2015 Oct 26) v3.0.22 (2015 Sept 9) ===================== -- Added method :meth:`text_search` which allows an API client to access the Shotgun global search +- Added method :meth:`text_search` which allows an API client to access the Shotgun global search and auto completer. -- Added method :meth:`activity_stream_read` which allows an API client to access the activity +- Added method :meth:`activity_stream_read` which allows an API client to access the activity stream for a given Shotgun entity. -- Added method :meth:`note_thread_read` which allows an API client to download an entire Note +- Added method :meth:`note_thread_read` which allows an API client to download an entire Note conversation, including Replies and Attachments, using a single API call. -- Added an experimental ``mockgun`` module which can be used to emulate the Shotgun API, for +- Added an experimental ``mockgun`` module which can be used to emulate the Shotgun API, for example inside unit test rigs. - [minor] Improved docstrings. @@ -313,23 +313,23 @@ v3.0.19 (2015 Mar 25) - Add ability to authenticate with Shotgun using ``session_token``. - Add :meth:`get_session_token` method for obtaining token to authenticate with. -- Add new ``AuthenticationFault`` exception type to indicate when server communication has failed +- Add new ``AuthenticationFault`` exception type to indicate when server communication has failed due to authentication reasons. -- Add support for ``SHOTGUN_API_CACERTS`` environment variable to provide location of external +- Add support for ``SHOTGUN_API_CACERTS`` environment variable to provide location of external SSL certificates file. - Fixes and updates to various tests. v3.0.18 (2015 Mar 13) ===================== -- Add ability to query the per-project visibility status for entities, fields and statuses. +- Add ability to query the per-project visibility status for entities, fields and statuses. (requires Shotgun server >= v5.4.4) v3.0.17 (2014 Jul 10) ===================== - Add ability to update ``last_accessed_by_current_user`` on Project. -- Add workaround for `bug #9291 in Python 2.7 `_ affecting +- Add workaround for `bug #9291 in Python 2.7 `_ affecting mimetypes library on Windows. - Add platform and Python version to user-agent (eg. ``shotgun-json (3.0.17); Python 2.7 (Mac)``) @@ -343,7 +343,7 @@ v3.0.16 (2014 May 23) v3.0.15 (2014 Mar 6) ==================== -- Fixed bug which allowed a value of ``None`` for password parameter in +- Fixed bug which allowed a value of ``None`` for password parameter in :meth:`authenticate_human_user` - Add :meth:`follow`, :meth:`unfollow` and :meth:`followers` methods. - Add ability to login as HumanUser. @@ -355,24 +355,24 @@ v3.0.14 (2013 Jun 26) ===================== - added: additional tests for thumbnails. -- added: support for downloading from s3 in :meth:`download_attachment`. Accepts an Attachment - entity dict as a parameter (is still backwards compatible with passing in an Attachment id). -- added: optional ``file_path`` parameter to :meth:`download_attachment` to write data directly to +- added: support for downloading from s3 in :meth:`download_attachment`. Accepts an Attachment + entity dict as a parameter (is still backwards compatible with passing in an Attachment id). +- added: optional ``file_path`` parameter to :meth:`download_attachment` to write data directly to disk instead of loading into memory. (thanks to Adam Goforth `@aag `_) v3.0.13 (2013 Apr 11) ===================== -- fixed: #20856 :meth:`authenticate_human_user` login was sticky and would be used for permissions +- fixed: #20856 :meth:`authenticate_human_user` login was sticky and would be used for permissions and logging. v3.0.12 (2013 Feb 22) ===================== *no tag* -- added: #18171 New ``ca_certs`` argument to the :class:`Shotgun` constructor to specify the +- added: #18171 New ``ca_certs`` argument to the :class:`Shotgun` constructor to specify the certificates to use in SSL validation. -- added: ``setup.py`` doesn't compress the installed ``.egg`` file which makes the +- added: ``setup.py`` doesn't compress the installed ``.egg`` file which makes the ``cacerts.txt`` file accessible. v3.0.11 (2013 Jan 31) @@ -383,21 +383,21 @@ v3.0.11 (2013 Jan 31) v3.0.10 (2013 Jan 25) ===================== -- added: :meth:`add_user_agent()` and :meth:`reset_user_agent` methods to allow client code to add +- added: :meth:`add_user_agent()` and :meth:`reset_user_agent` methods to allow client code to add strings to track. -- added: Changed default ``user-agent`` to include API version. +- added: Changed default ``user-agent`` to include API version. - updated: advanced summarize filter support. - fixed: #19830 :meth:`share_thumbnail` errors when source has no thumbnail. v3.0.9 (2012 Dec 05) ==================== -- added: :meth:`share_thumbnail` method to share the same thumbnail record and media between +- added: :meth:`share_thumbnail` method to share the same thumbnail record and media between entities. -- added: proxy handling to methods that transfer binary data (ie. :meth:`upload`, +- added: proxy handling to methods that transfer binary data (ie. :meth:`upload`, :meth:`upload_thumbnail`, etc.). - updated: default logging level to WARN. -- updated: documentation for :meth:`summarize()` method, previously released but without +- updated: documentation for :meth:`summarize()` method, previously released but without documentation. - fixed: unicode strings not always being encoded correctly. - fixed: :meth:`create()` generates error when ``return_fields`` is None. @@ -411,10 +411,10 @@ v3.0.9.beta2 (2012 Mar 19) ========================== - use relative imports for included libraries when using Python v2.5 or later. -- replace sideband request for ``image`` (thumbnail) field with native support (requires Shotgun - server >= v3.3.0. Request will still work on older versions but fallback to slow sideband +- replace sideband request for ``image`` (thumbnail) field with native support (requires Shotgun + server >= v3.3.0. Request will still work on older versions but fallback to slow sideband method). -- allow setting ``image`` and ``filmstrip_thumbnail`` in data dict on :meth:`create` and +- allow setting ``image`` and ``filmstrip_thumbnail`` in data dict on :meth:`create` and :meth:`update` (thanks `@hughmacdonald `_). - fixed bug causing ``Attachment.tag_list`` to be set to ``"None"`` (str) for uploads. @@ -433,7 +433,7 @@ v3.0.8 (2011 Oct 7) - added the :meth:`summarize` method. - refactored single file into package. - tests added (Thanks to Aaron Morton `@amorton `_). -- return all strings as ascii for backwards compatibility, added ``ensure_ascii`` parameter to +- return all strings as ascii for backwards compatibility, added ``ensure_ascii`` parameter to enable returning unicode. v3.0.7 (2011 Apr 04) @@ -473,7 +473,7 @@ v3.0.2 (2010 Aug 27) v3.0.1 (2010 May 10) ==================== -- :meth:`find`: default sorting to ascending, if not set (instead of requiring +- :meth:`find`: default sorting to ascending, if not set (instead of requiring ascending/descending). - :meth:`upload` and :meth:`upload_thumbnail`: pass auth info through. @@ -481,7 +481,7 @@ v3.0 (2010 May 5) ================= - non-beta! -- add :meth:`batch` method to do multiple :meth:`create`, :meth:`update`, and :meth:`delete` +- add :meth:`batch` method to do multiple :meth:`create`, :meth:`update`, and :meth:`delete` operations in one request to the server (requires Shotgun server to be v1.13.0 or higher). v3.0b8 (2010 Feb 19) @@ -498,7 +498,7 @@ v3.0b7 (2009 Nov 30) v3.0b6 (2009 Oct 20) ==================== -- add support for ``HTTP/1.1 keepalive``, which greatly improves performance for multiple +- add support for ``HTTP/1.1 keepalive``, which greatly improves performance for multiple requests. - add more helpful error if server entered is not ``http`` or ``https`` - add support assigning tags to file uploads (for Shotgun version >= 1.10.6). @@ -522,6 +522,6 @@ v3.0b3 (2009 June 24) - added ``schema_*`` methods for accessing entities and fields. - added support for http proxy servers. - added ``__version__`` string. -- removed ``RECORDS_PER_PAGE`` global (can just set ``records_per_page`` on the Shotgun object +- removed ``RECORDS_PER_PAGE`` global (can just set ``records_per_page`` on the Shotgun object after initializing it). - removed ``api_ver`` from the constructor, as this class is only designed to work with API v3. diff --git a/LICENSE b/LICENSE index a32a5bdcb..716d625d8 100644 --- a/LICENSE +++ b/LICENSE @@ -5,12 +5,12 @@ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - + list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - + - Neither the name of the Shotgun Software Inc nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. @@ -51,4 +51,4 @@ BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE -OF THIS SOFTWARE. \ No newline at end of file +OF THIS SOFTWARE. diff --git a/SECURITY.md b/SECURITY.md index c32c73245..0cf2a2664 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -32,4 +32,4 @@ configurations, reproduction steps, exploit code, impact, etc. ## Additional Information -Please check out the [Flow Production Tracking Security White Paper](https://help.autodesk.com/view/SGSUB/ENU/?guid=SG_Administrator_ar_general_security_ar_security_white_paper_html). \ No newline at end of file +Please check out the [Flow Production Tracking Security White Paper](https://help.autodesk.com/view/SGSUB/ENU/?guid=SG_Administrator_ar_general_security_ar_security_white_paper_html). diff --git a/azure-pipelines-templates/code_style_validation.yml b/azure-pipelines-templates/code_style_validation.yml new file mode 100644 index 000000000..69e82b7e0 --- /dev/null +++ b/azure-pipelines-templates/code_style_validation.yml @@ -0,0 +1,50 @@ +# Copyright (c) 2024, Shotgun Software Inc. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# - Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# - Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# - Neither the name of the Shotgun Software Inc nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +jobs: +- job: code_style_validation + displayName: Code Style Validation + pool: + vmImage: 'ubuntu-latest' + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: 3.9 + addToPath: True + architecture: 'x64' + + - script: | + pip install --upgrade pip setuptools wheel + pip install --upgrade pre-commit + displayName: Install dependencies + + - bash: pre-commit autoupdate + displayName: Update pre-commit hook versions + + - bash: pre-commit run --all + displayName: Validate code with pre-commit diff --git a/azure-pipelines-templates/run-tests.yml b/azure-pipelines-templates/run-tests.yml index edf5ffe4a..831c276ec 100644 --- a/azure-pipelines-templates/run-tests.yml +++ b/azure-pipelines-templates/run-tests.yml @@ -34,7 +34,7 @@ parameters: jobs: # The job will be named after the OS and Azure will suffix the strategy to make it unique # so we'll have a job name "Windows Python27" for example. What's a strategy? Strategies are the - # name of the keys under the strategy.matrix scope. So for each OS we'll have " Python27" and + # name of the keys under the strategy.matrix scope. So for each OS we'll have " Python27" and # " Python37". - job: ${{ parameters.name }} pool: @@ -63,7 +63,7 @@ jobs: # Specifies which version of Python we want to use. That's where the strategy comes in. # Each job will share this set of steps, but each of them will receive a different # $(python.version) - # TODO: We should provide `githubToken` if we want to download a python release. + # TODO: We should provide `githubToken` if we want to download a python release. # Otherwise we may hit the GitHub anonymous download limit. - task: UsePythonVersion@0 inputs: @@ -138,7 +138,7 @@ jobs: Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe -Outfile codecov.exe .\codecov.exe -f coverage.xml displayName: Uploading code coverage - - ${{ elseif eq(parameters.name, 'Linux') }}: + - ${{ elseif eq(parameters.name, 'Linux') }}: - script: | curl -Os https://uploader.codecov.io/latest/linux/codecov chmod +x codecov diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 989745755..52e6cfa9c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -51,6 +51,7 @@ pr: # This here is the list of jobs we want to run for our build. # Jobs run in parallel. jobs: +- template: azure-pipelines-templates/code_style_validation.yml # These are jobs templates, they allow to reduce the redundancy between # variations of the same build. We pass in the image name diff --git a/docs/advanced/iron_python.rst b/docs/advanced/iron_python.rst index 6aac0a6a9..62ad6d791 100644 --- a/docs/advanced/iron_python.rst +++ b/docs/advanced/iron_python.rst @@ -34,4 +34,4 @@ v3.0.20 can be used with IronPython with a little bit of added work: lower level SSL library backing python's network infrastructure is attempting to connect to our servers via SSLv3, which is no longer supported. You can use the code from this gist to force the SSL connections to use a specific protocol. The forked repo linked above has an example of how to - do that to force the use of TLSv1. \ No newline at end of file + do that to force the use of TLSv1. diff --git a/docs/advanced/packaging.rst b/docs/advanced/packaging.rst index d46426e73..8467db9e2 100644 --- a/docs/advanced/packaging.rst +++ b/docs/advanced/packaging.rst @@ -6,7 +6,7 @@ Packaging an application with py2app (or py2exe) You can create standalone applications with Python scripts by using `py2app `_ on OS X or `py2exe `_ on -Windows. This is often done to more easily distribute applications that have a GUI based on +Windows. This is often done to more easily distribute applications that have a GUI based on toolkits like Tk, Qt or others. There are caveats you need to be aware of when creating such an app. @@ -37,4 +37,4 @@ into the Flow Production Tracking connection's constructor:: sg = shotgun_api3.Shotgun('https://my-site.shotgrid.autodesk.com', 'script_name', 'script_key', ca_certs=ca_certs) -The process for py2exe should be similar. \ No newline at end of file +The process for py2exe should be similar. diff --git a/docs/authentication.rst b/docs/authentication.rst index 0e5fe8572..ea049fbd0 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -62,4 +62,3 @@ For Scripts, the default permission role is "API Admin User" which allows full a When using user-based authentication in your script, it will be bound by the permission role assigned to you in Flow Production Tracking. For example, if you don't have access to edit the status field on Shots, your script won't be able to either. Attempting to perform actions that are prohibited by permissions will raise an appropriate exception. .. seealso:: `Permissions Documentation `_ - diff --git a/docs/changelog.rst b/docs/changelog.rst index f07fa1f9c..3b4977908 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,3 +1,3 @@ .. currentmodule:: shotgun_api3.shotgun.Shotgun -.. include:: ../HISTORY.rst \ No newline at end of file +.. include:: ../HISTORY.rst diff --git a/docs/cookbook.rst b/docs/cookbook.rst index f69334a7b..fe0a5a300 100644 --- a/docs/cookbook.rst +++ b/docs/cookbook.rst @@ -3,7 +3,7 @@ API Cookbook ************ Here we have a collection of useful information you can use for reference when writing your API -scripts. From usage tips and gotchas to deeper examples of working with entities like Tasks and +scripts. From usage tips and gotchas to deeper examples of working with entities like Tasks and Files, there's a lot of example code in here for you to play with. .. rubric:: Usage Tips @@ -28,7 +28,7 @@ and paste any of these into your own scripts. .. rubric:: Working With Files -You'll probably be doing some work with files at your studio. This is a deep dive into some of +You'll probably be doing some work with files at your studio. This is a deep dive into some of the inners of how Flow Production Tracking handles files (also called Attachments) and the different ways to link to them. @@ -51,12 +51,12 @@ need to do. .. rubric:: Smart Cut Fields -Smart Cut Fields are deprecated in favor of the +Smart Cut Fields are deprecated in favor of the `new cut support added in ShotGrid v7.0 `_. This documentation remains only to support studios who may not have upgraded to the new cut support -features. +features. .. toctree:: :maxdepth: 2 - cookbook/smart_cut_fields \ No newline at end of file + cookbook/smart_cut_fields diff --git a/docs/cookbook/examples/ami_handler.rst b/docs/cookbook/examples/ami_handler.rst index f64ccd558..3fb5e3571 100644 --- a/docs/cookbook/examples/ami_handler.rst +++ b/docs/cookbook/examples/ami_handler.rst @@ -4,15 +4,15 @@ Handling Action Menu Item Calls ############################### -This is an example ActionMenu Python class to handle the ``GET`` request sent from an -ActionMenuItem. It doesn't manage dispatching custom protocols but rather takes the arguments -from any ``GET`` data and parses them into the easily accessible and correctly typed instance +This is an example ActionMenu Python class to handle the ``GET`` request sent from an +ActionMenuItem. It doesn't manage dispatching custom protocols but rather takes the arguments +from any ``GET`` data and parses them into the easily accessible and correctly typed instance variables for your Python scripts. Available as a Gist at https://gist.github.com/3253287 .. seealso:: - Our `support site has more information about Action Menu Items + Our `support site has more information about Action Menu Items `_. ************ diff --git a/docs/cookbook/examples/ami_version_packager.rst b/docs/cookbook/examples/ami_version_packager.rst index 5d3035014..415075a1d 100644 --- a/docs/cookbook/examples/ami_version_packager.rst +++ b/docs/cookbook/examples/ami_version_packager.rst @@ -4,7 +4,7 @@ Using an ActionMenuItem to Package Versions for a Client ######################################################## -This is an example script to demonstrate how you can use an ActionMenuItem to launch a local +This is an example script to demonstrate how you can use an ActionMenuItem to launch a local script to package up files for a client. It performs the following: - Downloads Attachments from a specified field for all selected entities. @@ -37,10 +37,10 @@ It is intended to be used in conjunction with the script dicussed in :ref:`ami_h login who ran the ActionMenuItem ('Demo_Project_2010-04-29-172210_kp.tar.gz'): sa = ShotgunAction(sys.argv[1]) - sg = shotgun_connect() + sg = shotgun_connect() if sa.action == 'package4client': r = packageFilesForClient('sg_qt','/path/where/i/want/to/put/the/archive/') - + """ # --------------------------------------------------------------------------------------------- @@ -61,8 +61,8 @@ It is intended to be used in conjunction with the script dicussed in :ref:`ami_h # --------------------------------------------------------------------------------------------- # Flow Production Tracking server auth info shotgun_conf = { - 'url':'https://my-site.shotgrid.autodesk.com', - 'name':'YOUR_SCRIPT_NAME_HERE', + 'url':'https://my-site.shotgrid.autodesk.com', + 'name':'YOUR_SCRIPT_NAME_HERE', 'key':'YOUR_SCRIPT_KEY_HERE' } @@ -70,9 +70,9 @@ It is intended to be used in conjunction with the script dicussed in :ref:`ami_h logfile = os.path.dirname(sys.argv[0])+"/version_packager.log" # temporary directory to download movie files to and create thumbnail files in - file_dir = os.path.dirname(sys.argv[0])+"/tmp" + file_dir = os.path.dirname(sys.argv[0])+"/tmp" - # compress command + # compress command # tar czf /home/user/backup_www.tar.gz -C / var/www/html compress_cmd = "tar czf %s -C / %s" @@ -89,7 +89,7 @@ It is intended to be used in conjunction with the script dicussed in :ref:`ami_h # ---------------------------------------------- # Set up logging # ---------------------------------------------- - def init_log(filename="version_packager.log"): + def init_log(filename="version_packager.log"): try: logger.basicConfig(level=logger.DEBUG, format='%(asctime)s %(levelname)-8s %(message)s', @@ -98,8 +98,8 @@ It is intended to be used in conjunction with the script dicussed in :ref:`ami_h filemode='w+') except IOError, e: raise ShotgunException ("Unable to open logfile for writing: %s" % e) - logger.info("Version Packager logging started.") - return logger + logger.info("Version Packager logging started.") + return logger # ---------------------------------------------- @@ -111,9 +111,9 @@ It is intended to be used in conjunction with the script dicussed in :ref:`ami_h try: attachment_id = int(attachment_id) except: - # not an integer. + # not an integer. return None - # raise ShotgunException("invalid Attachment id returned. Expected an integer: %s "% attachment_id) + # raise ShotgunException("invalid Attachment id returned. Expected an integer: %s "% attachment_id) return attachment_id @@ -126,16 +126,16 @@ It is intended to be used in conjunction with the script dicussed in :ref:`ami_h if type(attachment_id) != int: return None # download the attachment file from Flow Production Tracking and write it to local disk - logger.info("Downloading Attachment #%s" % (attachment_id)) + logger.info("Downloading Attachment #%s" % (attachment_id)) stream = sg.download_attachment(attachment_id) try: file = open(destination_filename, 'w') file.write(stream) file.close() logger.info("Downloaded attachment %s" % (destination_filename)) - return True + return True except e: - raise ShotgunException("unable to write attachment to disk: %s"% e) + raise ShotgunException("unable to write attachment to disk: %s"% e) # ---------------------------------------------- @@ -194,28 +194,28 @@ It is intended to be used in conjunction with the script dicussed in :ref:`ami_h logger.info("copied files to: %s" % destination_directory) return destination_directory - - + + def packageFilesForClient(file_field,destination_dir): - - # get entities matching the selected ids - logger.info("Querying Shotgun for %s %ss" % (len(sa.selected_ids_filter), sa.params['entity_type'])) + + # get entities matching the selected ids + logger.info("Querying Shotgun for %s %ss" % (len(sa.selected_ids_filter), sa.params['entity_type'])) entities = sg.find(sa.params['entity_type'],sa.selected_ids_filter,['id','code',file_field],filter_operator='any') - + # download the attachments for each entity, zip them, and copy to destination directory files = [] for e in entities: if not e[file_field]: - logger.info("%s #%s: No file exists. Skippinsa." % (sa.params['entity_type'], e['id'])) + logger.info("%s #%s: No file exists. Skippinsa." % (sa.params['entity_type'], e['id'])) else: - logger.info("%s #%s: %s" % (sa.params['entity_type'], e['id'], e[file_field])) + logger.info("%s #%s: %s" % (sa.params['entity_type'], e['id'], e[file_field])) path_to_file = file_dir+"/"+re.sub(r"\s+", '_', e[file_field]['name']) - result = download_attachment_to_disk(e[file_field], path_to_file ) - + result = download_attachment_to_disk(e[file_field], path_to_file ) + # only include attachments. urls won't return true if result: files.append(path_to_file) - + # compress files # create a nice valid destination filename project_name = '' @@ -223,7 +223,7 @@ It is intended to be used in conjunction with the script dicussed in :ref:`ami_h project_name = re.sub(r"\s+", '_', sa.params['project_name'])+'_' dest_filename = project_name+datetime.today().strftime('%Y-%m-%d-%H%M%S')+"_"+sa.params['user_login'] archive = compress_files(files,file_dir+"/"+dest_filename) - + # now that we have the archive, remove the downloads r = remove_downloaded_files(files) @@ -232,26 +232,25 @@ It is intended to be used in conjunction with the script dicussed in :ref:`ami_h return True - + # ---------------------------------------------- # Main Block # ---------------------------------------------- if __name__ == "__main__": init_log(logfile) - + try: sa = ShotgunAction(sys.argv[1]) logger.info("Firing... %s" % (sys.argv[1]) ) except IndexError, e: raise ShotgunException("Missing POST arguments") - - sg = Shotgun(shotgun_conf['url'], shotgun_conf['name'], shotgun_conf['key'],convert_datetimes_to_utc=convert_tz) - + + sg = Shotgun(shotgun_conf['url'], shotgun_conf['name'], shotgun_conf['key'],convert_datetimes_to_utc=convert_tz) + if sa.action == 'package4client': result = packageFilesForClient('sg_qt','/Users/kp/Documents/shotgun/dev/api/files/') else: raise ShotgunException("Unknown action... :%s" % sa.action) - - - print("\nVersion Packager done!") + + print("\nVersion Packager done!") diff --git a/docs/cookbook/examples/basic_create_shot.rst b/docs/cookbook/examples/basic_create_shot.rst index 4a9ece5f6..7513305fa 100644 --- a/docs/cookbook/examples/basic_create_shot.rst +++ b/docs/cookbook/examples/basic_create_shot.rst @@ -7,28 +7,28 @@ Building the data and calling :meth:`~shotgun_api3.Shotgun.create` ------------------------------------------------------------------ To create a Shot, you need to provide the following values: -- ``project`` is a link to the Project the Shot belongs to. It should be a dictionary like +- ``project`` is a link to the Project the Shot belongs to. It should be a dictionary like ``{"type": "Project", "id": 123}`` where ``id`` is the ``id`` of the Project. - ``code`` (this is the field that stores the name Shot) - optionally any other info you want to provide Example:: - data = { + data = { 'project': {"type":"Project","id": 4}, 'code': '100_010', 'description': 'Open on a beautiful field with fuzzy bunnies', - 'sg_status_list': 'ip' + 'sg_status_list': 'ip' } result = sg.create('Shot', data) This will create a new Shot named "100_010" in the Project "Gunslinger" (which has an ``id`` of 4). -- ``data`` is a list of key/value pairs where the key is the column name to update and the value +- ``data`` is a list of key/value pairs where the key is the column name to update and the value is the the value to set. - ``sg`` is the Flow Production Tracking API instance you created in :ref:`example_sg_instance`. -- ``create()`` is the :meth:`shotgun_api3.Shotgun.create` API method we are calling. We pass in the +- ``create()`` is the :meth:`shotgun_api3.Shotgun.create` API method we are calling. We pass in the entity type we're searching for and the data we're setting. .. rubric:: Result @@ -67,27 +67,27 @@ The Complete Example # Globals # -------------------------------------- # make sure to change this to match your Flow Production Tracking server and auth credentials. - SERVER_PATH = "https://my-site.shotgrid.autodesk.com" - SCRIPT_NAME = 'my_script' + SERVER_PATH = "https://my-site.shotgrid.autodesk.com" + SCRIPT_NAME = 'my_script' SCRIPT_KEY = '27b65d7063f46b82e670fe807bd2b6f3fd1676c1' # -------------------------------------- - # Main + # Main # -------------------------------------- - if __name__ == '__main__': + if __name__ == '__main__': sg = shotgun_api3.Shotgun(SERVER_PATH, SCRIPT_NAME, SCRIPT_KEY) # -------------------------------------- # Create a Shot with data # -------------------------------------- - data = { + data = { 'project': {"type":"Project","id": 4}, 'code': '100_010', 'description': 'Open on a beautiful field with fuzzy bunnies', - 'sg_status_list': 'ip' + 'sg_status_list': 'ip' } - result = sg.create('Shot', data) + result = sg.create('Shot', data) pprint(result) print("The id of the {} is {}.".format(result['type'], result['id'])) @@ -100,4 +100,3 @@ And here is the output:: 'sg_status_list': 'ip', 'type': 'Shot'} The id of the Shot is 40435. - diff --git a/docs/cookbook/examples/basic_create_shot_task_template.rst b/docs/cookbook/examples/basic_create_shot_task_template.rst index 0fa3b0828..ab6248227 100644 --- a/docs/cookbook/examples/basic_create_shot_task_template.rst +++ b/docs/cookbook/examples/basic_create_shot_task_template.rst @@ -14,7 +14,7 @@ First we need to find the Task Template we're going to apply. We'll assume you k The Resulting Task Template --------------------------- -Assuming the task template was found, we will now have something like this in our ``template`` +Assuming the task template was found, we will now have something like this in our ``template`` variable:: {'type': 'TaskTemplate', 'id': 12} @@ -30,16 +30,16 @@ Now we can create the Shot with the link to the ``TaskTemplate`` to apply. 'task_template': template } result = sg.create('Shot', data) -This will create a new Shot named "100_010" linked to the TaskTemplate "3D Shot Template" and +This will create a new Shot named "100_010" linked to the TaskTemplate "3D Shot Template" and Flow Production Tracking will then create the Tasks defined in the template and link them to the Shot you just created. -- ``data`` is a list of key/value pairs where the key is the column name to update and the value is +- ``data`` is a list of key/value pairs where the key is the column name to update and the value is the value. - ``project`` and `code` are required - ``description`` is just a text field that you might want to update as well -- ``task_template`` is another entity column where we set the Task Template which has the Tasks we - wish to create by default on this Shot. We found the specific template we wanted to assign in the +- ``task_template`` is another entity column where we set the Task Template which has the Tasks we + wish to create by default on this Shot. We found the specific template we wanted to assign in the previous block by searching Result @@ -59,7 +59,7 @@ The variable ``result`` now contains the dictionary of the new Shot that was cre } -If we now search for the Tasks linked to the Shot, we'll find the Tasks that match our +If we now search for the Tasks linked to the Shot, we'll find the Tasks that match our ``TaskTemplate``:: tasks = sg.find('Task', filters=[['entity', 'is', result]]) diff --git a/docs/cookbook/examples/basic_create_version_link_shot.rst b/docs/cookbook/examples/basic_create_version_link_shot.rst index 71e6f3e1e..3f025eb46 100644 --- a/docs/cookbook/examples/basic_create_version_link_shot.rst +++ b/docs/cookbook/examples/basic_create_version_link_shot.rst @@ -5,7 +5,7 @@ new ``Version`` entity linked to the Shot. Find the Shot ------------- -First we need to find the Shot since we'll need to know know its ``id`` in order to link our Version +First we need to find the Shot since we'll need to know know its ``id`` in order to link our Version to it. :: @@ -16,8 +16,8 @@ to it. Find the Task ------------- -Now we find the Task that the Version relates to, again so we can use the ``id`` to link it to the -Version we're creating. For this search we'll use the Shot ``id`` (which we have now in the ``shot`` +Now we find the Task that the Version relates to, again so we can use the ``id`` to link it to the +Version we're creating. For this search we'll use the Shot ``id`` (which we have now in the ``shot`` variable from the previous search) and the Task Name, which maps to the ``content`` field. :: @@ -27,7 +27,7 @@ variable from the previous search) and the Task Name, which maps to the ``conten task = sg.find_one('Task', filters) .. note:: Linking a Task to the Version is good practice. By doing so it is easy for users to see - at what stage a particular Version was created, and opens up other possibilities for tracking + at what stage a particular Version was created, and opens up other possibilities for tracking in Flow Production Tracking. We highly recommend doing this whenever possible. Create the Version @@ -44,22 +44,22 @@ Now we can create the Version with the link to the Shot and the Task:: 'user': {'type': 'HumanUser', 'id': 165} } result = sg.create('Version', data) -This will create a new Version named '100_010_anim_v1' linked to the 'Animation' Task for Shot +This will create a new Version named '100_010_anim_v1' linked to the 'Animation' Task for Shot '100_010' in the Project 'Gunslinger'. -- ``data`` is a list of key/value pairs where the key is the column name to update and the value is +- ``data`` is a list of key/value pairs where the key is the column name to update and the value is the value to set. - ``project`` and ``code`` are required -- ``description`` and ``sg_path_to_frames`` are just text fields that you might want to update as +- ``description`` and ``sg_path_to_frames`` are just text fields that you might want to update as well -- ``sg_status_list`` is the status column for the Version. Here we are setting it to "rev" (Pending +- ``sg_status_list`` is the status column for the Version. Here we are setting it to "rev" (Pending Review) so that it will get reviewed in the next dailies session and people will "ooh" and "aaah". -- ``entity`` is where we link this version to the Shot. Entity columns are always handled with this +- ``entity`` is where we link this version to the Shot. Entity columns are always handled with this format. You must provide the entity ``type`` and its ``id``. -- ``sg_task`` is another entity link field specifically for the Version's Task link. This uses the +- ``sg_task`` is another entity link field specifically for the Version's Task link. This uses the same entity format as the Shot link, but pointing to the Task entity this time. -- ``user`` is another entity column where we set the artist responsible for this masterpiece. In - this example, I know the 'id' that corresponds to this user, but if you don't know the id you can +- ``user`` is another entity column where we set the artist responsible for this masterpiece. In + this example, I know the 'id' that corresponds to this user, but if you don't know the id you can look it up by searching on any of the fields, similar to what we did for the Shot above, like:: filters = [['login', 'is', 'jschmoe']] @@ -72,11 +72,11 @@ The ``result`` variable now contains the ``id`` of the new Version that was crea Upload a movie for review in Screening Room ------------------------------------------- -If Screening Room's transcoding feature is enabled on your site (hosted sites have this by -default), then you can use the :meth:`~shotgun_api3.Shotgun.upload` method to upload a QuickTime -movie, PDF, still image, etc. to the ``sg_uploaded_movie`` field on a Version. Once the movie is -uploaded, it will automatically be queued for transcoding. When transcoding is complete, the -Version will be playable in the Screening Room app, or in the Overlay player by clicking on the +If Screening Room's transcoding feature is enabled on your site (hosted sites have this by +default), then you can use the :meth:`~shotgun_api3.Shotgun.upload` method to upload a QuickTime +movie, PDF, still image, etc. to the ``sg_uploaded_movie`` field on a Version. Once the movie is +uploaded, it will automatically be queued for transcoding. When transcoding is complete, the +Version will be playable in the Screening Room app, or in the Overlay player by clicking on the Play button that will appear on the Version's thumbnail. -.. note:: Transcoding also generates a thumbnail and filmstrip thumbnail automatically. \ No newline at end of file +.. note:: Transcoding also generates a thumbnail and filmstrip thumbnail automatically. diff --git a/docs/cookbook/examples/basic_delete_shot.rst b/docs/cookbook/examples/basic_delete_shot.rst index 5275735d6..4f2e91018 100644 --- a/docs/cookbook/examples/basic_delete_shot.rst +++ b/docs/cookbook/examples/basic_delete_shot.rst @@ -5,7 +5,7 @@ Calling :meth:`~shotgun_api3.Shotgun.delete` -------------------------------------------- Deleting an entity in Flow Production Tracking is pretty straight-forward. No extraneous steps required.:: - result = sg.delete("Shot", 40435) + result = sg.delete("Shot", 40435) Result ------ @@ -30,23 +30,22 @@ The Complete Example # -------------------------------------- # make sure to change this to match your Flow Production Tracking server and auth credentials. SERVER_PATH = "https://my-site.shotgrid.autodesk.com" - SCRIPT_NAME = 'my_script' + SCRIPT_NAME = 'my_script' SCRIPT_KEY = '27b65d7063f46b82e670fe807bd2b6f3fd1676c1' # -------------------------------------- - # Main + # Main # -------------------------------------- - if __name__ == '__main__': + if __name__ == '__main__': sg = shotgun_api3.Shotgun(SERVER_PATH, SCRIPT_NAME, SCRIPT_KEY) # -------------------------------------- # Delete a Shot by id # -------------------------------------- - result = sg.delete("Shot", 40435) + result = sg.delete("Shot", 40435) pprint(result) And here is the output:: True - diff --git a/docs/cookbook/examples/basic_find_shot.rst b/docs/cookbook/examples/basic_find_shot.rst index 88c8c81e0..945eb1be6 100644 --- a/docs/cookbook/examples/basic_find_shot.rst +++ b/docs/cookbook/examples/basic_find_shot.rst @@ -12,7 +12,7 @@ We are going to assume we know the 'id' of the Shot we're looking for in this ex Pretty simple right? Well here's a little more insight into what's going on. -- ``filters`` is an list of filter conditions. In this example we are filtering for Shots where +- ``filters`` is an list of filter conditions. In this example we are filtering for Shots where the ``id`` column is **40435**. - ``sg`` is the Flow Production Tracking API instance. - ``find_one()`` is the :meth:`~shotgun_api3.Shotgun.find_one` API method we are calling. We @@ -25,13 +25,13 @@ So what does this return? The variable result now contains:: {'type': 'Shot','id': 40435} -By default, :meth:`~shotgun_api3.Shotgun.find_one` returns a single dictionary object with +By default, :meth:`~shotgun_api3.Shotgun.find_one` returns a single dictionary object with the ``type`` and ``id`` fields. So in this example, we found a Shot matching that id, and Flow Production Tracking returned it as a dictionary object with ``type`` and ``id`` keys . -How do we know that result contains the Shot dictionary object? You can trust us... but just to be -sure, the :mod:`pprint` (PrettyPrint) module from the Python standard library is a really good tool -to help with debugging. It will print out objects in a nicely formatted way that makes things +How do we know that result contains the Shot dictionary object? You can trust us... but just to be +sure, the :mod:`pprint` (PrettyPrint) module from the Python standard library is a really good tool +to help with debugging. It will print out objects in a nicely formatted way that makes things easier to read. So we'll add that to the import section of our script.:: import shotgun_api3 @@ -54,13 +54,13 @@ The Complete Example # -------------------------------------- # make sure to change this to match your Flow Production Tracking server and auth credentials. SERVER_PATH = "https://my-site.shotgrid.autodesk.com" - SCRIPT_NAME = 'my_script' + SCRIPT_NAME = 'my_script' SCRIPT_KEY = '27b65d7063f46b82e670fe807bd2b6f3fd1676c1' # -------------------------------------- - # Main + # Main # -------------------------------------- - if __name__ == '__main__': + if __name__ == '__main__': sg = shotgun_api3.Shotgun(SERVER_PATH, SCRIPT_NAME, SCRIPT_KEY) @@ -68,7 +68,7 @@ The Complete Example # Find a Shot by id # -------------------------------------- filters = [['id', 'is', 40435]] - result = sg.find_one('Shot', filters) + result = sg.find_one('Shot', filters) pprint(result) And here is the output:: diff --git a/docs/cookbook/examples/basic_sg_instance.rst b/docs/cookbook/examples/basic_sg_instance.rst index b39c78432..d17b57de5 100644 --- a/docs/cookbook/examples/basic_sg_instance.rst +++ b/docs/cookbook/examples/basic_sg_instance.rst @@ -13,14 +13,14 @@ authentication. ``sg`` represents your Flow Production Tracking API instance. Be import shotgun_api3 SERVER_PATH = "https://my-site.shotgrid.autodesk.com" - SCRIPT_NAME = 'my_script' + SCRIPT_NAME = 'my_script' SCRIPT_KEY = '27b65d7063f46b82e670fe807bd2b6f3fd1676c1' sg = shotgun_api3.Shotgun(SERVER_PATH, SCRIPT_NAME, SCRIPT_KEY) - # Just for demo purposes, this will print out property and method names available on the + # Just for demo purposes, this will print out property and method names available on the # sg connection object pprint.pprint([symbol for symbol in sorted(dir(sg)) if not symbol.startswith('_')]) For further information on what you can do with this Flow Production Tracking object you can read the -:ref:`API reference `. \ No newline at end of file +:ref:`API reference `. diff --git a/docs/cookbook/examples/basic_update_shot.rst b/docs/cookbook/examples/basic_update_shot.rst index 52e57e70e..c2413c3ee 100644 --- a/docs/cookbook/examples/basic_update_shot.rst +++ b/docs/cookbook/examples/basic_update_shot.rst @@ -3,23 +3,23 @@ Update A Shot Building the data and calling :meth:`~shotgun_api3.Shotgun.update` ------------------------------------------------------------------ -To update a Shot, you need to provide the ``id`` of the Shot and a list of fields you want to +To update a Shot, you need to provide the ``id`` of the Shot and a list of fields you want to update.:: - data = { + data = { 'description': 'Open on a beautiful field with fuzzy bunnies', - 'sg_status_list': 'ip' + 'sg_status_list': 'ip' } result = sg.update('Shot', 40435, data) -This will update the ``description`` and the ``sg_status_list`` fields for the Shot with ``id`` of +This will update the ``description`` and the ``sg_status_list`` fields for the Shot with ``id`` of **40435**. - ``data`` is a list of key/value pairs where the key is the field name to update and the value to update it to. - ``sg`` is the Flow Production Tracking API instance. -- ``update()`` is the :meth:`shotgun_api3.Shotgun.update` API method we are calling. We provide it - with the entity type we're updating, the ``id`` of the entity, and the data we're updating it +- ``update()`` is the :meth:`shotgun_api3.Shotgun.update` API method we are calling. We provide it + with the entity type we're updating, the ``id`` of the entity, and the data we're updating it with. Result @@ -34,7 +34,7 @@ The variable ``result`` now contains the Shot object that with the updated value } In addition, Flow Production Tracking has returned the ``id`` for the Shot, as well as a ``type`` value. ``type`` -is provided for convenience simply to help you identify what entity type this dictionary represents. +is provided for convenience simply to help you identify what entity type this dictionary represents. It does not correspond to any field in Flow Production Tracking. Flow Production Tracking will *always* return the ``id`` and ``type`` keys in the dictionary when there are results @@ -57,24 +57,24 @@ The Complete Example # -------------------------------------- # make sure to change this to match your Flow Production Tracking server and auth credentials. SERVER_PATH = "https://my-site.shotgrid.autodesk.com" - SCRIPT_NAME = 'my_script' + SCRIPT_NAME = 'my_script' SCRIPT_KEY = '27b65d7063f46b82e670fe807bd2b6f3fd1676c1' # -------------------------------------- - # Main + # Main # -------------------------------------- - if __name__ == '__main__': + if __name__ == '__main__': sg = shotgun_api3.Shotgun(SERVER_PATH, SCRIPT_NAME, SCRIPT_KEY) # -------------------------------------- # Update Shot with data # -------------------------------------- - data = { + data = { 'description': 'Open on a beautiful field with fuzzy bunnies', - 'sg_status_list': 'ip' + 'sg_status_list': 'ip' } - result = sg.update('Shot', 40435, data) + result = sg.update('Shot', 40435, data) pprint(result) And here is the output:: @@ -83,4 +83,3 @@ And here is the output:: 'id': 40435, 'sg_status_list': 'ip', 'type': 'Shot'} - diff --git a/docs/cookbook/examples/basic_upload_thumbnail_version.rst b/docs/cookbook/examples/basic_upload_thumbnail_version.rst index 2ae399d5f..ba7337150 100644 --- a/docs/cookbook/examples/basic_upload_thumbnail_version.rst +++ b/docs/cookbook/examples/basic_upload_thumbnail_version.rst @@ -2,11 +2,11 @@ Upload a Thumbnail for a Version ================================ So you've created a new Version of a Shot, and you've updated Flow Production Tracking, but now you want to upload a -beauty frame to display as the thumbnail for your Version. We'll assume you already have the image -made (located on your machine at ``/v1/gun/s100/010/beauties/anim/100_010_animv1.jpg``) . And since +beauty frame to display as the thumbnail for your Version. We'll assume you already have the image +made (located on your machine at ``/v1/gun/s100/010/beauties/anim/100_010_animv1.jpg``) . And since you've just created your Version in Flow Production Tracking, you know its ``id`` is **214**. -.. note:: If you upload a movie file or image to the ``sg_uploaded_movie`` field and you have +.. note:: If you upload a movie file or image to the ``sg_uploaded_movie`` field and you have transcoding enabled on your server (the default for hosted sites), a thumbnail will be generated automatically as well as a filmstrip thumbnail (if possible). This is a basic example of how to manually provide or replace a thumbnail image. @@ -21,6 +21,6 @@ Upload the Image using :meth:`~shotgun_api3.Shotgun.upload_thumbnail` Flow Production Tracking will take care of resizing the thumbnail for you. If something does go wrong, an exception will be thrown and you'll see the error details. -.. note:: The result returned by :meth:`~shotgun_api3.Shotgun.upload_thumbnail` is an integer +.. note:: The result returned by :meth:`~shotgun_api3.Shotgun.upload_thumbnail` is an integer representing the id of a special ``Attachment`` entity in Flow Production Tracking. Working with Attachments - is beyond the scope of this example. :) \ No newline at end of file + is beyond the scope of this example. :) diff --git a/docs/cookbook/examples/svn_integration.rst b/docs/cookbook/examples/svn_integration.rst index 9a877b322..8b0a6ce46 100644 --- a/docs/cookbook/examples/svn_integration.rst +++ b/docs/cookbook/examples/svn_integration.rst @@ -9,26 +9,26 @@ Integrating Flow Production Tracking with Subversion consists of two basic parts - Setup a post-commit hook in Subversion. - Create a Flow Production Tracking API script to create the Revision in Flow Production Tracking. This script will be called by the post-commit hook. - + **************** Post-Commit Hook **************** To setup the post-commit hook: -- Locate the ``post-commit.tmpl`` file, which is found inside the ``hooks`` folder in your - repository directory. This is a template script that has lots of useful comments and can serve +- Locate the ``post-commit.tmpl`` file, which is found inside the ``hooks`` folder in your + repository directory. This is a template script that has lots of useful comments and can serve as a starting point for the real thing. -- Create your very own executable script, and save it in the same ``hooks`` folder, name it +- Create your very own executable script, and save it in the same ``hooks`` folder, name it ``post-commit``, and give it executable permission. - In your ``post-commit`` script, invoke your Flow Production Tracking API script. -If this is entirely new to you, we highly suggest reading up on the topic. O'Reilly has `a free -online guide for Subversion 1.5 and 1.6 +If this is entirely new to you, we highly suggest reading up on the topic. O'Reilly has `a free +online guide for Subversion 1.5 and 1.6 `_ -Here's an example of a post-commit hook that we've made for Subversion 1.6 using an executable -Unix shell script. The last line invokes "shotgun_api_script.py" which is our Python script that +Here's an example of a post-commit hook that we've made for Subversion 1.6 using an executable +Unix shell script. The last line invokes "shotgun_api_script.py" which is our Python script that will do all the heavy lifting. Lines 4 thru 8 queue up some objects that we'll use later on. .. code-block:: sh @@ -48,13 +48,13 @@ will do all the heavy lifting. Lines 4 thru 8 queue up some objects that we'll Explanation of selected lines ============================= -- lines ``4-5``: After the commit, Subversion leaves us two string objects in the environment: - ``REPOS`` and ``REV`` (the repository path and the revision number, respectively). -- lines ``7-8``: Here we use the shell command ``export`` to create two more string objects in the - environment: ``AUTHOR`` and ``COMMENT``. To get each value, we use the ``svnlook`` command with - our ``REPOS`` and ``REV`` values, first with the ``author``, and then with ``log`` subcommand. - These are actually the first two original lines of code - everything else to this point was - pre-written already in the ``post-commit.tmpl`` file. nice :) +- lines ``4-5``: After the commit, Subversion leaves us two string objects in the environment: + ``REPOS`` and ``REV`` (the repository path and the revision number, respectively). +- lines ``7-8``: Here we use the shell command ``export`` to create two more string objects in the + environment: ``AUTHOR`` and ``COMMENT``. To get each value, we use the ``svnlook`` command with + our ``REPOS`` and ``REV`` values, first with the ``author``, and then with ``log`` subcommand. + These are actually the first two original lines of code - everything else to this point was + pre-written already in the ``post-commit.tmpl`` file. nice :) - line ``10``: This is the absolute path to our Flow Production Tracking API Script. *********************************** @@ -63,7 +63,7 @@ Flow Production Tracking API Script This script will create the Revision and populate it with some metadata using the Flow Production Tracking Python API. It will create our Revision in Flow Production Tracking along with the author, comment, and because we use -Trac (a web-based interface for Subversion), it will also populate a URL field with a clickable +Trac (a web-based interface for Subversion), it will also populate a URL field with a clickable link to the Revision. .. code-block:: python @@ -84,27 +84,27 @@ link to the Revision. # Globals - update all of these values to those of your studio # --------------------------------------------------------------------------------------------- SERVER_PATH = 'https ://my-site.shotgrid.autodesk.com' # or http: - SCRIPT_USER = 'script_name' + SCRIPT_USER = 'script_name' SCRIPT_KEY = '3333333333333333333333333333333333333333' REVISIONS_PATH = 'https ://serveraddress/trac/changeset/' # or other web-based UI PROJECT = {'type':'Project', 'id':27} - + # --------------------------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------------------------- if __name__ == '__main__': sg = Shotgun(SERVER_PATH, SCRIPT_USER, SCRIPT_KEY) - + # Set Python variables from the environment objects revision_code = os.environ['REV'] repository = os.environ['REPOS'] description = os.environ['COMMENT'] author = os.environ['AUTHOR'] - + # Set the Trac path for this specific revision revision_url = REVISIONS_PATH + revision_code - + # Validate that author is a valid Flow Production Tracking HumanUser result = sg.find_one("HumanUser", [['login', 'is', author]]) if result: @@ -118,7 +118,7 @@ link to the Revision. } revision = sg.create("Revision", parameters) print("created Revision #"+str(revision_code)) - + # Send error message if valid HumanUser is not found else: print("Unable to find a valid Flow Production Tracking User with login: {}, Revision not created in Flow Production Tracking.".format(author)) @@ -131,16 +131,16 @@ Explanation of selected lines: - line ``14``: This should be the URL to your instance of Flow Production Tracking. - lines ``15-16``: Make sure you get these values from the "Scripts" page in the Admin section of the Flow Production Tracking web application. If you're not sure how to do this, check out :doc:`authentication`. -- line ``17``: This is the address of Trac, our web-based interface that we use with Subversion. - You may use a different interface, or none at all, so feel free to adjust this line or ignore it +- line ``17``: This is the address of Trac, our web-based interface that we use with Subversion. + You may use a different interface, or none at all, so feel free to adjust this line or ignore it as your case may be. - line ``18``: Every Revision in Flow Production Tracking must have a Project, which is passed to the API as a - dictionary with two keys, the ``type`` and the ``id``. Of course the ``type`` value will always - remain ``Project`` (case sensitive), but the ``id`` will change by Project. To find out the + dictionary with two keys, the ``type`` and the ``id``. Of course the ``type`` value will always + remain ``Project`` (case sensitive), but the ``id`` will change by Project. To find out the ``id`` of your Project, go to the Projects page in the Flow Production Tracking web application, locate the - Project where you want your Revisions created, and then locate its ``id`` field (which you may - need to display - if you don't see it, right click on any column header then select - "Insert Column" > "Id"). Note that for this example we assume that all Revisions in this + Project where you want your Revisions created, and then locate its ``id`` field (which you may + need to display - if you don't see it, right click on any column header then select + "Insert Column" > "Id"). Note that for this example we assume that all Revisions in this Subversion repository will belong to the same Project. - lines ``28-31``: Grab the values from the objects that were left for us in the environment. - line ``34``: Add the Revision number to complete the path of our Trac url. @@ -148,10 +148,10 @@ Explanation of selected lines: Users' Flow Production Tracking logins match their Subversion names. If the user exists in Flow Production Tracking, that user's ``id`` will be returned as ``result['id']``, which we will need later on in line 46. - lines ``40-48``: Use all the meta data we've gathered to create a Revision in Flow Production Tracking. If none - of these lines make any sense, check out more on the :meth:`~shotgun_api3.Shotgun.create` method - here. Line 41 deserves special mention: notice that we define a dictionary called ``url`` that - has three important keys: ``content_type``, ``url``, and ``name``, and we then pass this in as - the value for the ``attachment`` field when we create the Revision. If you're even in doubt, + of these lines make any sense, check out more on the :meth:`~shotgun_api3.Shotgun.create` method + here. Line 41 deserves special mention: notice that we define a dictionary called ``url`` that + has three important keys: ``content_type``, ``url``, and ``name``, and we then pass this in as + the value for the ``attachment`` field when we create the Revision. If you're even in doubt, double check the syntax and requirements for the different field types here. *************** @@ -161,8 +161,8 @@ Troubleshooting My post-commit script is simply not running. I can run it manually, but commits are not triggering it. ====================================================================================================== -Make sure that the script is has explicitly been made executable and that all users who will -invoke it have appropriate permissions for the script and that folders going back to root. +Make sure that the script is has explicitly been made executable and that all users who will +invoke it have appropriate permissions for the script and that folders going back to root. My Flow Production Tracking API script is not getting called by the post-commit hook. ===================================================================================== diff --git a/docs/cookbook/smart_cut_fields.rst b/docs/cookbook/smart_cut_fields.rst index 0ee74d189..928cf3b53 100644 --- a/docs/cookbook/smart_cut_fields.rst +++ b/docs/cookbook/smart_cut_fields.rst @@ -9,16 +9,16 @@ Smart Cut Fields cut support. `Read the Cut Support Documentation here `_. If you want to work with 'smart' cut fields through the API, your script should use a corresponding -'raw' fields for all updates. The 'smart_cut_fields' are primarily for display in the UI, the real +'raw' fields for all updates. The 'smart_cut_fields' are primarily for display in the UI, the real data is stored in a set of 'raw' fields that have different names. ************ Smart Fields ************ -In the UI these fields attempt to calculate values based on data entered into the various fields. -These fields can be queried via the API using the find() method, but not updated. Note that we are -deprecating this feature and recommend creating your own cut fields from scratch, which will not +In the UI these fields attempt to calculate values based on data entered into the various fields. +These fields can be queried via the API using the find() method, but not updated. Note that we are +deprecating this feature and recommend creating your own cut fields from scratch, which will not contain any calculations which have proven to be too magical at times. - ``smart_cut_duration`` diff --git a/docs/cookbook/tasks.rst b/docs/cookbook/tasks.rst index 0acb5f5e7..dd3a1426d 100644 --- a/docs/cookbook/tasks.rst +++ b/docs/cookbook/tasks.rst @@ -2,12 +2,12 @@ Working With Tasks ################## -Tasks have various special functionality available in the UI that can also be queried and +Tasks have various special functionality available in the UI that can also be queried and manipulated through the API. The sections below cover these topics. .. toctree:: :maxdepth: 2 - + tasks/updating_tasks tasks/task_dependencies tasks/split_tasks diff --git a/docs/cookbook/tasks/split_tasks.rst b/docs/cookbook/tasks/split_tasks.rst index 45dfc303f..d16c50e94 100644 --- a/docs/cookbook/tasks/split_tasks.rst +++ b/docs/cookbook/tasks/split_tasks.rst @@ -4,15 +4,15 @@ Split Tasks ########### -Split tasks can be created and edited via the API but must comply to some rules. Before going +Split tasks can be created and edited via the API but must comply to some rules. Before going further, a good understanding of :ref:`how Flow Production Tracking handles task dates is useful `. ******** Overview ******** -The Task entity has a field called ``splits`` which is a list of dictionaries. Each dictionary -in the list has two string keys, ``start`` and ``end``, who's values are strings representing dates +The Task entity has a field called ``splits`` which is a list of dictionaries. Each dictionary +in the list has two string keys, ``start`` and ``end``, who's values are strings representing dates in the ``YYYY-mm-dd`` format. :: @@ -21,11 +21,11 @@ in the ``YYYY-mm-dd`` format. - Splits should be ordered from eldest to newest. - There should be gaps between each split. - - - Gaps are defined as at least one working day. Non-workdays such as weekends and holidays + + - Gaps are defined as at least one working day. Non-workdays such as weekends and holidays are not gaps. -If there are multiple splits but there between two or more splits there is no gap, an error will be +If there are multiple splits but there between two or more splits there is no gap, an error will be raised. For example:: >>> sg.update('Task', 2088, {'splits':[{'start':'2012-12-10', 'end':'2012-12-11'}, {'start':'2012-12-12', 'end':'2012-12-14'}, {'start':'2012-12-19', 'end':'2012-12-20'}]}) @@ -40,7 +40,7 @@ raised. For example:: shotgun_api3.shotgun.Fault: API update() CRUD ERROR #5: Update failed for [Task.splits]: (task.rb) The start date in split segment 2 is only one calendar day away from the end date of the previous segment. There must be calendar days between split segments. Alternately, a split value can be set to ``None`` to remove splits (you can also use an empty list). -This will preserve the ``start_date`` and ``due_date`` values but recalculate the ``duration`` value +This will preserve the ``start_date`` and ``due_date`` values but recalculate the ``duration`` value while appropriately considering all workday rules in effect. ******************************************************** @@ -50,16 +50,16 @@ How Do Splits Influence Dates And Dates Influence Splits - If splits are specified the supplied ``start_date``, ``due_date`` and ``duration`` fields will be ignored. - The ``start_date`` will be inferred from the earliest split. - The ``due_date`` will be inferred from the last split. -- If the ``start_date`` is changed on a task that has splits the first split will be moved to start - on the new ``start_date`` and all further splits will be moved while maintaining gap lengths +- If the ``start_date`` is changed on a task that has splits the first split will be moved to start + on the new ``start_date`` and all further splits will be moved while maintaining gap lengths between splits and respecting workday rules. -- If the ``due_date`` is changed on a task that has splits the last split will be moved to end on - the new ``due_date`` and all prior splits will be moved while maintaining gap lengths between +- If the ``due_date`` is changed on a task that has splits the last split will be moved to end on + the new ``due_date`` and all prior splits will be moved while maintaining gap lengths between splits and respecting workday rules. - If the ``duration`` is changed two scenarios are possible - + - In the case of a longer duration, additional days will be added to the end of the last split - - In the case of a shorter duration splits, starting with the latest ones, will be either + - In the case of a shorter duration splits, starting with the latest ones, will be either removed or shortened until the new duration is met. Examples @@ -216,7 +216,7 @@ Result: Setting the due_date in a gap ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When a due date is set in a gap later splits are removed and the day of the due date is considered +When a due date is set in a gap later splits are removed and the day of the due date is considered a day when work will be done. For this example let's assume as a starting point the result of the 5th example: @@ -242,16 +242,3 @@ For this example let's assume as a starting point the result of the 5th example: Result: .. image:: /images/split_tasks_9.png - - - - - - - - - - - - - diff --git a/docs/cookbook/tasks/task_dependencies.rst b/docs/cookbook/tasks/task_dependencies.rst index a5cfc20be..bf6f5e610 100644 --- a/docs/cookbook/tasks/task_dependencies.rst +++ b/docs/cookbook/tasks/task_dependencies.rst @@ -4,16 +4,16 @@ Task Dependencies ################# -Task dependencies work the same way in the API as they do in the UI. You can filter and sort on +Task dependencies work the same way in the API as they do in the UI. You can filter and sort on any of the fields. For information about Task Dependencies in Flow Production Tracking, check out the `main -documentation page on our support site +documentation page on our support site `_ ************ Create Tasks ************ -Let's create a couple of Tasks and create dependencies between them. First we'll create a "Layout" +Let's create a couple of Tasks and create dependencies between them. First we'll create a "Layout" Task for our Shot:: data = { @@ -22,7 +22,7 @@ Task for our Shot:: 'start_date': '2010-04-28', 'due_date': '2010-05-05', 'entity': {'type':'Shot', 'id':860} - } + } result = sg.create(Task, data) @@ -45,7 +45,7 @@ Now let's create an "Anm" Task for our Shot:: 'start_date': '2010-05-06', 'due_date': '2010-05-12', 'entity': {'type':'Shot', 'id':860} - } + } result = sg.create(Task, data) Returns:: @@ -63,11 +63,11 @@ Returns:: Create A Dependency ******************* -Tasks each have an ``upstream_tasks`` field and a ``downstream_tasks`` field. Each field is a -list ``[]`` type and can contain zero, one, or multiple Task entity dictionaries representing the +Tasks each have an ``upstream_tasks`` field and a ``downstream_tasks`` field. Each field is a +list ``[]`` type and can contain zero, one, or multiple Task entity dictionaries representing the dependent Tasks. There are four dependency types from which you can choose: ``finish-to-start-next-day``, ``start-to-finish-next-day``, ``start-to-start``, ``finish-to-finish``. -If no dependency type is provided the default ``finish-to-start-next-day`` will be used. +If no dependency type is provided the default ``finish-to-start-next-day`` will be used. Here is how to create a dependency between our "Layout" and "Anm" Tasks:: # make 'Layout' an upstream Task to 'Anm'. (aka, make 'Anm' dependent on 'Layout') with finish-to-start-next-day dependency type @@ -85,7 +85,7 @@ Returns:: This will also automatically update the `downstream_tasks` field on 'Layout' to include the 'Anm' Task. *********************** -Query Task Dependencies +Query Task Dependencies *********************** Task Dependencies each have a ``dependent_task_id`` and a ``task_id`` fields. @@ -127,7 +127,7 @@ So now lets look at the Tasks we've created and their dependency-related fields: 'due_date', 'upstream_tasks', 'downstream_tasks', - 'dependency_violation', + 'dependency_violation', 'pinned' ] result = sg.find("Task", filters, fields) @@ -151,17 +151,17 @@ Returns:: 'pinned': False, 'start_date': '2010-05-06', 'type': 'Task', - 'upstream_tasks': [{'type': 'Task', 'name': 'Layout', 'id': 556}]}, - ... + 'upstream_tasks': [{'type': 'Task', 'name': 'Layout', 'id': 556}]}, + ... -*Note that we have also created additional Tasks for this Shot but we're going to focus on these +*Note that we have also created additional Tasks for this Shot but we're going to focus on these first two for simplicity.* ***************************************************************** Updating the End Date on a Task with Downstream Task Dependencies ***************************************************************** -If we update the ``due_date`` field on our "Layout" Task, we'll see that the "Anm" Task dates +If we update the ``due_date`` field on our "Layout" Task, we'll see that the "Anm" Task dates will automatically get pushed back to keep the dependency satisfied:: result = sg.update('Task', 556, {'due_date': '2010-05-07'}) @@ -189,20 +189,20 @@ Our Tasks now look like this (notice the new dates on the "Anm" Task):: 'pinned': False, 'start_date': '2010-05-10', 'type': 'Task', - 'upstream_tasks': [{'type': 'Task', 'name': 'Layout', 'id': 556}]}, - ... + 'upstream_tasks': [{'type': 'Task', 'name': 'Layout', 'id': 556}]}, + ... ********************************************************** Creating a Dependency Violation by pushing up a Start Date ********************************************************** -Task Dependencies can work nicely if you are pushing out an end date for a Task as it will just -recalculate the dates for all of the dependent Tasks. But what if we push up the Start Date of our +Task Dependencies can work nicely if you are pushing out an end date for a Task as it will just +recalculate the dates for all of the dependent Tasks. But what if we push up the Start Date of our "Anm" Task to start before our "Layout" Task is scheduled to end? :: - + result = sg.update('Task', 557, {'start_date': '2010-05-06'}) Returns:: @@ -229,21 +229,21 @@ Our Tasks now look like this:: 'start_date': '2010-05-06', 'type': 'Task', 'upstream_tasks': [{'type': 'Task', 'name': 'Layout', 'id': 556}]}, - ... + ... -Because the "Anm" Task ``start_date`` depends on the ``due_date`` of the "Layout" Task, this +Because the "Anm" Task ``start_date`` depends on the ``due_date`` of the "Layout" Task, this change creates a dependency violation. The update succeeds, but Flow Production Tracking has also set the -``dependency_violation`` field to ``True`` and has also updated the ``pinned`` field to ``True``. +``dependency_violation`` field to ``True`` and has also updated the ``pinned`` field to ``True``. -The ``pinned`` field simply means that if the upstream Task(s) are moved, the "Anm" Task will no -longer get moved with it. The dependency is still there (in ``upstream_tasks``) but the Task is +The ``pinned`` field simply means that if the upstream Task(s) are moved, the "Anm" Task will no +longer get moved with it. The dependency is still there (in ``upstream_tasks``) but the Task is now "pinned" to those dates until the Dependency Violation is resolved. *********************************************************** -Resolving a Dependency Violation by updating the Start Date +Resolving a Dependency Violation by updating the Start Date *********************************************************** -We don't want that violation there. Let's revert that change so the Start Date for "Anm" is after +We don't want that violation there. Let's revert that change so the Start Date for "Anm" is after the End Date of "Layout":: result = sg.update('Task', 557, {'start_date': '2010-05-10'}) @@ -272,10 +272,10 @@ Our Tasks now look like this:: 'start_date': '2010-05-10', 'type': 'Task', 'upstream_tasks': [{'type': 'Task', 'name': 'Layout', 'id': 556}]}, - ... + ... -The ``dependency_violation`` field has now been set back to ``False`` since there is no longer -a violation. But notice that the ``pinned`` field is still ``True``. We will have to manually +The ``dependency_violation`` field has now been set back to ``False`` since there is no longer +a violation. But notice that the ``pinned`` field is still ``True``. We will have to manually update that if we want the Task to travel with its dependencies again:: result = sg.update('Task', 557, {'pinned': False}) @@ -304,19 +304,19 @@ Our Tasks now look like this:: 'start_date': '2010-05-10', 'type': 'Task', 'upstream_tasks': [{'type': 'Task', 'name': 'Layout', 'id': 556}]}, - ... + ... -Looks great. But that's an annoying manual process. What if we want to just reset a Task so that +Looks great. But that's an annoying manual process. What if we want to just reset a Task so that it automatically gets updated so that the Start Date is after its dependent Tasks? ******************************************************************* Updating the ``pinned`` field on a Task with a Dependency Violation ******************************************************************* -Let's go back a couple of steps to where our "Anm" Task had a Dependency Violation because we had -moved the Start Date up before the "Layout" Task End Date. Remember that the ``pinned`` field +Let's go back a couple of steps to where our "Anm" Task had a Dependency Violation because we had +moved the Start Date up before the "Layout" Task End Date. Remember that the ``pinned`` field was also ``True``. If we simply update the ``pinned`` field to be ``False``, Flow Production Tracking will also -automatically update the Task dates to satisfy the upstream dependencies and reset the +automatically update the Task dates to satisfy the upstream dependencies and reset the ``dependency_violation`` value to ``False``:: result = sg.update('Task', 557, {'pinned': False}) @@ -345,19 +345,19 @@ Our Tasks now look like this:: 'pinned': False, 'start_date': '2010-05-10', 'type': 'Task', - 'upstream_tasks': [{'type': 'Task', 'name': 'Layout', 'id': 556}]}, - ... + 'upstream_tasks': [{'type': 'Task', 'name': 'Layout', 'id': 556}]}, + ... Notice by updating ``pinned`` to ``False``, Flow Production Tracking also updated the ``start_date`` and -``due_date`` fields of our "Anm" Task so it will satisfy the upstream Task dependencies. And since +``due_date`` fields of our "Anm" Task so it will satisfy the upstream Task dependencies. And since that succeeded, the ``dependency_violation`` field has also been set to ``False`` ******************************************* ``dependency_violation`` field is read-only ******************************************* -The ``dependency_violation`` field is the only dependency-related field that is read-only. Trying +The ``dependency_violation`` field is the only dependency-related field that is read-only. Trying to modify it will generate a Fault:: result = sg.update('Task', 557, {'dependency_violation': False}) diff --git a/docs/cookbook/tasks/updating_tasks.rst b/docs/cookbook/tasks/updating_tasks.rst index 97eb8c7b7..c7c216e3f 100644 --- a/docs/cookbook/tasks/updating_tasks.rst +++ b/docs/cookbook/tasks/updating_tasks.rst @@ -4,9 +4,9 @@ Updating Task Dates: How Flow Production Tracking Thinks ######################################################## -When updating Task dates in an API update() request, there is no specified order to the values that +When updating Task dates in an API update() request, there is no specified order to the values that are passed in. Flow Production Tracking also does automatic calculation of the``start_date``,``due_date``, and ``duration`` fields for convenience. In order to clarify how updates are handled by Flow Production Tracking we are -providing some general rules below and examples of what will happen when you make updates to your +providing some general rules below and examples of what will happen when you make updates to your Tasks. ************** @@ -17,31 +17,31 @@ General Rules - Updating the ``due_date`` automatically updates the ``duration`` (``start_date`` remains constant) - Updating the ``duration`` automatically updates the ``due_date`` (``start_date`` remains constant) - When updating Task values, Flow Production Tracking sets schedule fields (``milestone``, ``duration``, - ``start_date``, ``due_date``) after all other fields, because the Project and Task Assignees + ``start_date``, ``due_date``) after all other fields, because the Project and Task Assignees affect schedule calculations. -- If ``start_date`` and ``due_date`` are both set, ``duration`` is ignored (``duration`` can often +- If ``start_date`` and ``due_date`` are both set, ``duration`` is ignored (``duration`` can often be wrong since it's easy to calculate scheduling incorrectly). - If both ``start_date`` and ``due_date`` are provided, Flow Production Tracking sets ``start_date`` before ``due_date``. -- Set ``milestone`` before other schedule fields (because ``start_date``, ``due_date``, and +- Set ``milestone`` before other schedule fields (because ``start_date``, ``due_date``, and ``duration`` get lost if ``milestone`` is not set to ``False`` first) - If ``milestone`` is being set to ``True``, ``duration`` is ignored. -- If ``milestone`` is set to ``True`` and ``start_date`` and ``due_date`` are also being set to +- If ``milestone`` is set to ``True`` and ``start_date`` and ``due_date`` are also being set to conflicting values, an Exception is raised. -- If ``due_date`` and ``duration`` are set together (without ``start_date``), ``duration`` is set - first, then ``due_date`` (otherwise setting ``duration`` will change ``due_date`` after it is +- If ``due_date`` and ``duration`` are set together (without ``start_date``), ``duration`` is set + first, then ``due_date`` (otherwise setting ``duration`` will change ``due_date`` after it is set). ******** Examples ******** -The following examples show what the resulting Task object will look like after being run on the +The following examples show what the resulting Task object will look like after being run on the initial Task object listed under the header of each section. The ``duration`` values in the following examples assume your Flow Production Tracking instance is set to -10-hour work days. If your server is configured with a different setting, the ``duration`` values -will vary. +10-hour work days. If your server is configured with a different setting, the ``duration`` values +will vary. .. note:: The ``duration`` field stores ``duration`` values in minutes @@ -56,7 +56,7 @@ Regardless of current values on the Task, this behavior rules:: **Update start_date and due_date** -``duration`` is ignored if also provided. It is instead set automatically as (``due_date`` - +``duration`` is ignored if also provided. It is instead set automatically as (``due_date`` - ``start_date``) :: @@ -66,7 +66,7 @@ Regardless of current values on the Task, this behavior rules:: - ``start_date`` is updated. - ``due_date`` is updated. -- ``duration`` is calculated as (``due_date`` - ``start_date``) +- ``duration`` is calculated as (``due_date`` - ``start_date``) .. note:: The value provided in the update() is ignored (and in this case was also incorrect). @@ -90,7 +90,7 @@ Regardless of current values on the Task, this behavior rules:: - ``duration`` is updated. - ``due_date`` is updated. -- ``duration`` is calculated as (``due_date`` - ``start_date``) +- ``duration`` is calculated as (``due_date`` - ``start_date``) .. note:: This means the ``duration`` provided is overwritten. @@ -226,7 +226,7 @@ If the Task has ``start_date`` and ``due_date`` values but has no ``duration``, will behave. :: - + # Task = {'start_date': '2011-05-20', 'due_date': '2011-05-25', 'duration': None, 'id':123} **Update start_date** @@ -310,7 +310,7 @@ If the Task has ``due_date`` and ``duration`` values but has no ``start_date``, will behave. :: - + # Task = {'start_date': None, 'due_date': '2011-05-25', 'duration': 2400, 'id':123} **Update start_date** @@ -383,4 +383,4 @@ will behave. # Task = {'start_date': '2011-05-20', 'due_date': '2011-05-27', 'duration': 3600, 'id':123} - ``duration`` is updated. -- ``due_date`` is updated to (``start_date`` + ``duration``) \ No newline at end of file +- ``due_date`` is updated to (``start_date`` + ``duration``) diff --git a/docs/cookbook/tutorials.rst b/docs/cookbook/tutorials.rst index 99f56da02..2adbd4fbf 100644 --- a/docs/cookbook/tutorials.rst +++ b/docs/cookbook/tutorials.rst @@ -2,7 +2,7 @@ Examples ######## -Here's a list of various simple tutorials to walk through that should provide you with a good base +Here's a list of various simple tutorials to walk through that should provide you with a good base understanding of how to use the Flow Production Tracking API and what you can do with it. ***** diff --git a/docs/cookbook/usage_tips.rst b/docs/cookbook/usage_tips.rst index 91cd6e8cb..5d1a7bc1f 100644 --- a/docs/cookbook/usage_tips.rst +++ b/docs/cookbook/usage_tips.rst @@ -3,9 +3,9 @@ API Usage Tips ############## Below is a list of helpful tips when using the Flow Production Tracking API3. We have tried to make the API very -simple to use with predictable results while remaining a powerful tool to integrate with your -pipeline. However, there's always a couple of things that crop up that our users might not be -aware of. Those are the types of things you'll find below. We'll be adding to this document over +simple to use with predictable results while remaining a powerful tool to integrate with your +pipeline. However, there's always a couple of things that crop up that our users might not be +aware of. Those are the types of things you'll find below. We'll be adding to this document over time as new questions come up from our users that exhibit these types of cases. ********* @@ -43,13 +43,13 @@ the entities are returned in a standard dictionary:: {'type': 'Asset', 'name': 'redBall', 'id': 1} -For each entity returned, you will get a ``type``, ``name``, and ``id`` key. This does not mean -there are fields named ``type`` and ``name`` on the Asset. These are only used to provide a +For each entity returned, you will get a ``type``, ``name``, and ``id`` key. This does not mean +there are fields named ``type`` and ``name`` on the Asset. These are only used to provide a consistent way to represent entities returned via the API. - ``type``: the entity type (CamelCase) - ``name``: the display name of the entity. For most entity types this is the value of the ``code`` - field but not always. For example, on the Ticket and Delivery entities the ``name`` key would + field but not always. For example, on the Ticket and Delivery entities the ``name`` key would contain the value of the ``title`` field. .. _custom_entities: @@ -100,14 +100,14 @@ Connection entities exist behind the scenes for any many-to-many relationship. M you won't need to pay any attention to them. But in some cases, you may need to track information on the instance of one entity's relationship to another. -For example, when viewing a list of Versions on a Playlist, the Sort Order (``sg_sort_order``) field is an +For example, when viewing a list of Versions on a Playlist, the Sort Order (``sg_sort_order``) field is an example of a field that resides on the connection entity between Playlists and Versions. This -connection entity is appropriately called `PlaylistVersionConnection`. Because any Version can -exist in multiple Playlists, the sort order isn't specific to the Version, it's specific to -each _instance_ of the Version in a Playlist. These instances are tracked using connection +connection entity is appropriately called `PlaylistVersionConnection`. Because any Version can +exist in multiple Playlists, the sort order isn't specific to the Version, it's specific to +each _instance_ of the Version in a Playlist. These instances are tracked using connection entities in Shtogun and are accessible just like any other entity type in Flow Production Tracking. -To find information about your Versions in the Playlist "Director Review" (let's say it has an +To find information about your Versions in the Playlist "Director Review" (let's say it has an ``id`` of 4). We'd run a query like so:: filters = [['playlist', 'is', {'type':'Playlist', 'id':4}]] @@ -169,9 +169,9 @@ Which returns the following:: - ``playlist`` is the Playlist record for this connection instance. - ``sg_sort_order`` is the sort order field on the connection instance. -We can pull in field values from the linked Playlist and Version entities using dot notation like -``version.Version.code``. The syntax is ``fieldname.EntityType.fieldname``. In this example, -``PlaylistVersionConnection`` has a field named ``version``. That field contains a ``Version`` +We can pull in field values from the linked Playlist and Version entities using dot notation like +``version.Version.code``. The syntax is ``fieldname.EntityType.fieldname``. In this example, +``PlaylistVersionConnection`` has a field named ``version``. That field contains a ``Version`` entity. The field we are interested on the Version is ``code``. Put those together with our f riend the dot and we have ``version.Version.code``. @@ -179,20 +179,20 @@ riend the dot and we have ``version.Version.code``. Flow Production Tracking UI fields not available via the API ************************************************************ -Summary type fields like Query Fields and Pipeline Step summary fields are currently only available -via the UI. Some other fields may not work as expected through the API because they are "display +Summary type fields like Query Fields and Pipeline Step summary fields are currently only available +via the UI. Some other fields may not work as expected through the API because they are "display only" fields made available for convenience and are only available in the browser UI. HumanUser ========= -- ``name``: This is a UI-only field that is a combination of the ``firstname`` + ``' '`` + +- ``name``: This is a UI-only field that is a combination of the ``firstname`` + ``' '`` + ``lastname``. Shot ==== -**Smart Cut Fields**: These fields are available only in the browser UI. You can read more about +**Smart Cut Fields**: These fields are available only in the browser UI. You can read more about smart cut fields and the API in the :ref:`Smart Cut Fields doc `:: smart_cut_in @@ -212,25 +212,25 @@ smart cut fields and the API in the :ref:`Smart Cut Fields doc `_. This allows you to write plug-ins that watch for certain types of events and then run code when they occur. - + Structure of Event Types ======================== @@ -872,57 +872,57 @@ The basic structure of event types is broken into 3 parts: - ``Application``: Is always "Shotgun" for events automatically created by the Flow Production Tracking server. Other Flow Production Tracking products may use their name in here, for example, Toolkit has its own events - that it logs and the application portion is identified by "Toolkit". If you decide to use the + that it logs and the application portion is identified by "Toolkit". If you decide to use the EventLogEntry entity to log events for your scripts or tools, you would use your tool name here. - ``EntityType``: This is the entity type in Flow Production Tracking that was acted upon (eg. Shot, Asset, etc.) -- ``Action``: The general action that was taken. (eg. New, Change, Retirement, Revival) - +- ``Action``: The general action that was taken. (eg. New, Change, Retirement, Revival) + Standard Event Types ==================== -Each entity type has a standard set of events associated with it when it's created, updated, +Each entity type has a standard set of events associated with it when it's created, updated, deleted, and revived. They follow this pattern: - ``Shotgun_EntityType_New``: a new entity was created. Example: ``Shotgun_Task_New`` - ``Shotgun_EntityType_Change``: an entity was modified. Example: ``Shotgun_HumanUser_Change`` - ``Shotgun_EntityType_Retirement``: an entity was deleted. Example: ``Shotgun_Ticket_Retirement`` -- ``Shotgun_EntityType_Revival``: an entity was revived. Example: ``Shotgun_CustomEntity03_Revival`` +- ``Shotgun_EntityType_Revival``: an entity was revived. Example: ``Shotgun_CustomEntity03_Revival`` Additional Event Types ====================== These are _some_ of the additional event types that are logged by Flow Production Tracking: - + - ``Shotgun_Attachment_View``: an Attachment (file) was viewed by a user. -- ``Shotgun_Reading_Change``: a threaded entity has been marked read or unread. For example, a - Note was read by a user. The readings are unique to the entity<->user connection so when a +- ``Shotgun_Reading_Change``: a threaded entity has been marked read or unread. For example, a + Note was read by a user. The readings are unique to the entity<->user connection so when a Note is read by user "joe" it may still be unread by user "jane". - ``Shotgun_User_Login``: a user logged in to Flow Production Tracking. - ``Shotgun_User_Logout``: a user logged out of Flow Production Tracking. - + Custom Event Types ================== -Since ``EventLogEntries`` are entities themselves, you can create them using the API just like any -other entity type. As mentioned previously, if you'd like to have your scripts or tools log to +Since ``EventLogEntries`` are entities themselves, you can create them using the API just like any +other entity type. As mentioned previously, if you'd like to have your scripts or tools log to the Flow Production Tracking event log, simply devise a thoughtful naming structure for your event types and create the EventLogEntry as needed following the usual methods for creating entities via the API. Again, other Flow Production Tracking products like Toolkit use event logs this way. -.. note:: - EventLogEntries cannot be updated or deleted (that would defeat the purpose of course). - +.. note:: + EventLogEntries cannot be updated or deleted (that would defeat the purpose of course). + Performance =========== Event log database tables can get large very quickly. While Flow Production Tracking does very well with event logs -that get into the millions of records, there's an inevitable degradation of performance for pages -that display them in the web application as well as any API queries for events when they get too -big. This volume of events is not the norm, but can be reached if your server expereinces high -usage. +that get into the millions of records, there's an inevitable degradation of performance for pages +that display them in the web application as well as any API queries for events when they get too +big. This volume of events is not the norm, but can be reached if your server expereinces high +usage. This **does not** mean your Flow Production Tracking server performance will suffer in general, just any pages that are specifically displaying EventLogEntries in the web application, or API queries on the event @@ -976,7 +976,7 @@ Will internally be transformed as if you invoked something like this: .. code-block:: python - sg.find('Asset', [['project', 'is', {'id': 999, 'type': 'Project'}]]) + sg.find('Asset', [['project', 'is', {'id': 999, 'type': 'Project'}]]) ************ diff --git a/nose.cfg b/nose.cfg index 59c3f0974..22c0e11cd 100644 --- a/nose.cfg +++ b/nose.cfg @@ -9,4 +9,4 @@ # not expressly granted therein are reserved by Shotgun Software Inc. [nosetests] -exclude-dir=shotgun_api3/lib \ No newline at end of file +exclude-dir=shotgun_api3/lib diff --git a/run-tests b/run-tests index dbe93f8b2..61b0f82c9 100755 --- a/run-tests +++ b/run-tests @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + # Copyright (c) 2019 Shotgun Software Inc. # # CONFIDENTIAL AND PROPRIETARY diff --git a/setup.py b/setup.py index 337e3b13b..f92018fe1 100644 --- a/setup.py +++ b/setup.py @@ -12,25 +12,25 @@ import sys from setuptools import setup, find_packages -f = open('README.md') +f = open("README.md") readme = f.read().strip() -f = open('LICENSE') +f = open("LICENSE") license = f.read().strip() setup( - name='shotgun_api3', - version='3.8.0', - description='Flow Production Tracking Python API', + name="shotgun_api3", + version="3.8.0", + description="Flow Production Tracking Python API", long_description=readme, - author='Autodesk', - author_email='https://www.autodesk.com/support/contact-support', - url='https://github.com/shotgunsoftware/python-api', + author="Autodesk", + author_email="https://www.autodesk.com/support/contact-support", + url="https://github.com/shotgunsoftware/python-api", license=license, - packages=find_packages(exclude=('tests',)), + packages=find_packages(exclude=("tests",)), script_args=sys.argv[1:], include_package_data=True, - package_data={'': ['cacerts.txt', 'cacert.pem']}, + package_data={"": ["cacerts.txt", "cacert.pem"]}, zip_safe=False, python_requires=">=3.7.0", classifiers=[ diff --git a/shotgun_api3/__init__.py b/shotgun_api3/__init__.py index 49e96db7a..d296aa97a 100644 --- a/shotgun_api3/__init__.py +++ b/shotgun_api3/__init__.py @@ -8,9 +8,18 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. -from .shotgun import (Shotgun, ShotgunError, ShotgunFileDownloadError, # noqa unused imports - ShotgunThumbnailNotReady, Fault, - AuthenticationFault, MissingTwoFactorAuthenticationFault, - UserCredentialsNotAllowedForSSOAuthenticationFault, - ProtocolError, ResponseError, Error, __version__) -from .shotgun import SG_TIMEZONE as sg_timezone # noqa unused imports +from .shotgun import ( + Shotgun, + ShotgunError, + ShotgunFileDownloadError, # noqa unused imports + ShotgunThumbnailNotReady, + Fault, + AuthenticationFault, + MissingTwoFactorAuthenticationFault, + UserCredentialsNotAllowedForSSOAuthenticationFault, + ProtocolError, + ResponseError, + Error, + __version__, +) +from .shotgun import SG_TIMEZONE as sg_timezone # noqa unused imports diff --git a/shotgun_api3/shotgun.py b/shotgun_api3/shotgun.py index f0d4faf48..a805fa5f4 100644 --- a/shotgun_api3/shotgun.py +++ b/shotgun_api3/shotgun.py @@ -1,56 +1,56 @@ #!/usr/bin/env python """ - ----------------------------------------------------------------------------- - Copyright (c) 2009-2019, Shotgun Software Inc. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - - Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - - Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - - Neither the name of the Shotgun Software Inc nor the names of its - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +----------------------------------------------------------------------------- +Copyright (c) 2009-2019, Shotgun Software Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + - Neither the name of the Shotgun Software Inc nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ # Python 2/3 compatibility from .lib import six from .lib import sgsix from .lib import sgutils -from .lib.six import BytesIO # used for attachment upload +from .lib.six import BytesIO # used for attachment upload from .lib.six.moves import map from .lib.six.moves import http_cookiejar # used for attachment upload import datetime import logging -import uuid # used for attachment upload +import uuid # used for attachment upload import os import re import copy import ssl -import stat # used for attachment upload +import stat # used for attachment upload import sys import time import json from .lib.six.moves import urllib -import shutil # used for attachment download -from .lib.six.moves import http_client # Used for secure file upload. +import shutil # used for attachment download +from .lib.six.moves import http_client # Used for secure file upload. from .lib.httplib2 import Http, ProxyInfo, socks, ssl_error_classes from .lib.sgtimezone import SgTimezone @@ -88,9 +88,13 @@ def _is_mimetypes_broken(): # http://bugs.python.org/issue9291 <- Fixed in 2.7.7 # http://bugs.python.org/issue21652 <- Fixed in 2.7.8 # http://bugs.python.org/issue22028 <- Fixed in 2.7.10 - return (sys.platform == "win32" and - sys.version_info[0] == 2 and sys.version_info[1] == 7 and - sys.version_info[2] >= 0 and sys.version_info[2] <= 9) + return ( + sys.platform == "win32" + and sys.version_info[0] == 2 + and sys.version_info[1] == 7 + and sys.version_info[2] >= 0 + and sys.version_info[2] <= 9 + ) if _is_mimetypes_broken(): @@ -101,7 +105,7 @@ def _is_mimetypes_broken(): # mimetypes imported in version specific imports mimetypes.add_type("video/webm", ".webm") # webm and mp4 seem to be missing -mimetypes.add_type("video/mp4", ".mp4") # from some OS/distros +mimetypes.add_type("video/mp4", ".mp4") # from some OS/distros SG_TIMEZONE = SgTimezone() @@ -128,6 +132,7 @@ class ShotgunError(Exception): """ Base for all Shotgun API Errors. """ + pass @@ -135,6 +140,7 @@ class ShotgunFileDownloadError(ShotgunError): """ Exception for file download-related errors. """ + pass @@ -142,6 +148,7 @@ class ShotgunThumbnailNotReady(ShotgunError): """ Exception for when trying to use a 'pending thumbnail' (aka transient thumbnail) in an operation """ + pass @@ -149,6 +156,7 @@ class Fault(ShotgunError): """ Exception when server-side exception detected. """ + pass @@ -156,6 +164,7 @@ class AuthenticationFault(Fault): """ Exception when the server side reports an error related to authentication. """ + pass @@ -164,6 +173,7 @@ class MissingTwoFactorAuthenticationFault(Fault): Exception when the server side reports an error related to missing two-factor authentication credentials. """ + pass @@ -172,6 +182,7 @@ class UserCredentialsNotAllowedForSSOAuthenticationFault(Fault): Exception when the server is configured to use SSO. It is not possible to use a username/password pair to authenticate on such server. """ + pass @@ -180,8 +191,10 @@ class UserCredentialsNotAllowedForOxygenAuthenticationFault(Fault): Exception when the server is configured to use Oxygen. It is not possible to use a username/password pair to authenticate on such server. """ + pass + # ---------------------------------------------------------------------------- # API @@ -221,10 +234,12 @@ def __init__(self, host, meta): except AttributeError: self.version = None if not self.version: - raise ShotgunError("The Flow Production Tracking Server didn't respond with a version number. " - "This may be because you are running an older version of " - "Flow Production Tracking against a more recent version of the Flow Production Tracking API. " - "For more information, please contact the Autodesk support.") + raise ShotgunError( + "The Flow Production Tracking Server didn't respond with a version number. " + "This may be because you are running an older version of " + "Flow Production Tracking against a more recent version of the Flow Production Tracking API. " + "For more information, please contact the Autodesk support." + ) if len(self.version) > 3 and self.version[3] == "Dev": self.is_dev = True @@ -258,7 +273,12 @@ def _ensure_support(self, feature, raise_hell=True): if raise_hell: raise ShotgunError( "%s requires server version %s or higher, " - "server is %s" % (feature["label"], _version_str(feature["version"]), _version_str(self.version)) + "server is %s" + % ( + feature["label"], + _version_str(feature["version"]), + _version_str(self.version), + ) ) return False else: @@ -268,68 +288,62 @@ def _ensure_json_supported(self): """ Ensures server has support for JSON API endpoint added in v2.4.0. """ - self._ensure_support({ - "version": (2, 4, 0), - "label": "JSON API" - }) + self._ensure_support({"version": (2, 4, 0), "label": "JSON API"}) def ensure_include_archived_projects(self): """ Ensures server has support for archived Projects feature added in v5.3.14. """ - self._ensure_support({ - "version": (5, 3, 14), - "label": "include_archived_projects parameter" - }) + self._ensure_support( + {"version": (5, 3, 14), "label": "include_archived_projects parameter"} + ) def ensure_per_project_customization(self): """ Ensures server has support for per-project customization feature added in v5.4.4. """ - return self._ensure_support({ - "version": (5, 4, 4), - "label": "project parameter" - }, True) + return self._ensure_support( + {"version": (5, 4, 4), "label": "project parameter"}, True + ) def ensure_support_for_additional_filter_presets(self): """ Ensures server has support for additional filter presets feature added in v7.0.0. """ - return self._ensure_support({ - "version": (7, 0, 0), - "label": "additional_filter_presets parameter" - }, True) + return self._ensure_support( + {"version": (7, 0, 0), "label": "additional_filter_presets parameter"}, True + ) def ensure_user_following_support(self): """ Ensures server has support for listing items a user is following, added in v7.0.12. """ - return self._ensure_support({ - "version": (7, 0, 12), - "label": "user_following parameter" - }, True) + return self._ensure_support( + {"version": (7, 0, 12), "label": "user_following parameter"}, True + ) def ensure_paging_info_without_counts_support(self): """ Ensures server has support for optimized pagination, added in v7.4.0. """ - return self._ensure_support({ - "version": (7, 4, 0), - "label": "optimized pagination" - }, False) + return self._ensure_support( + {"version": (7, 4, 0), "label": "optimized pagination"}, False + ) def ensure_return_image_urls_support(self): """ Ensures server has support for returning thumbnail URLs without additional round-trips, added in v3.3.0. """ - return self._ensure_support({ - "version": (3, 3, 0), - "label": "return thumbnail URLs" - }, False) + return self._ensure_support( + {"version": (3, 3, 0), "label": "return thumbnail URLs"}, False + ) def __str__(self): - return "ServerCapabilities: host %s, version %s, is_dev %s"\ - % (self.host, self.version, self.is_dev) + return "ServerCapabilities: host %s, version %s, is_dev %s" % ( + self.host, + self.version, + self.is_dev, + ) class ClientCapabilities(object): @@ -379,9 +393,11 @@ def __init__(self): pass def __str__(self): - return "ClientCapabilities: platform %s, local_path_field %s, "\ - "py_verison %s, ssl version %s" % (self.platform, self.local_path_field, - self.py_version, self.ssl_version) + return ( + "ClientCapabilities: platform %s, local_path_field %s, " + "py_verison %s, ssl version %s" + % (self.platform, self.local_path_field, self.py_version, self.ssl_version) + ) class _Config(object): @@ -459,14 +475,11 @@ def set_server_params(self, base_url): :raises ValueError: Raised if protocol is not http or https. """ - self.scheme, self.server, api_base, _, _ = \ - urllib.parse.urlsplit(base_url) + self.scheme, self.server, api_base, _, _ = urllib.parse.urlsplit(base_url) if self.scheme not in ("http", "https"): - raise ValueError( - "base_url must use http or https got '%s'" % base_url - ) - self.api_path = urllib.parse.urljoin(urllib.parse.urljoin( - api_base or "/", self.api_ver + "/"), "json" + raise ValueError("base_url must use http or https got '%s'" % base_url) + self.api_path = urllib.parse.urljoin( + urllib.parse.urljoin(api_base or "/", self.api_ver + "/"), "json" ) @property @@ -477,7 +490,9 @@ def records_per_page(self): if self._records_per_page is None: # Check for api_max_entities_per_page in the server info and change the record per page # value if it is supplied. - self._records_per_page = self._sg.server_info.get("api_max_entities_per_page") or 500 + self._records_per_page = ( + self._sg.server_info.get("api_max_entities_per_page") or 500 + ) return self._records_per_page @@ -489,30 +504,32 @@ class Shotgun(object): # reg ex from # http://underground.infovark.com/2008/07/22/iso-date-validation-regex/ # Note a length check is done before checking the reg ex - _DATE_PATTERN = re.compile( - r"^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$") + _DATE_PATTERN = re.compile(r"^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$") _DATE_TIME_PATTERN = re.compile( r"^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])" - r"(\D?([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3})?)?$") + r"(\D?([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3})?)?$" + ) _MULTIPART_UPLOAD_CHUNK_SIZE = 20000000 - MAX_ATTEMPTS = 3 # Retries on failure - BACKOFF = 0.75 # Seconds to wait before retry, times the attempt number - - def __init__(self, - base_url, - script_name=None, - api_key=None, - convert_datetimes_to_utc=True, - http_proxy=None, - ensure_ascii=True, - connect=True, - ca_certs=None, - login=None, - password=None, - sudo_as_login=None, - session_token=None, - auth_token=None): + MAX_ATTEMPTS = 3 # Retries on failure + BACKOFF = 0.75 # Seconds to wait before retry, times the attempt number + + def __init__( + self, + base_url, + script_name=None, + api_key=None, + convert_datetimes_to_utc=True, + http_proxy=None, + ensure_ascii=True, + connect=True, + ca_certs=None, + login=None, + password=None, + sudo_as_login=None, + session_token=None, + auth_token=None, + ): """ Initializes a new instance of the Shotgun client. @@ -597,16 +614,19 @@ def __init__(self, # verify authentication arguments if session_token is not None: if script_name is not None or api_key is not None: - raise ValueError("cannot provide both session_token " - "and script_name/api_key") + raise ValueError( + "cannot provide both session_token " "and script_name/api_key" + ) if login is not None or password is not None: - raise ValueError("cannot provide both session_token " - "and login/password") + raise ValueError( + "cannot provide both session_token " "and login/password" + ) if login is not None or password is not None: if script_name is not None or api_key is not None: - raise ValueError("cannot provide both login/password " - "and script_name/api_key") + raise ValueError( + "cannot provide both login/password " "and script_name/api_key" + ) if login is None: raise ValueError("password provided without login") if password is None: @@ -620,15 +640,24 @@ def __init__(self, if auth_token is not None: if login is None or password is None: - raise ValueError("must provide a user login and password with an auth_token") + raise ValueError( + "must provide a user login and password with an auth_token" + ) if script_name is not None or api_key is not None: raise ValueError("cannot provide an auth_code with script_name/api_key") # Can't use 'all' with python 2.4 - if len([x for x in [session_token, script_name, api_key, login, password] if x]) == 0: + if ( + len( + [x for x in [session_token, script_name, api_key, login, password] if x] + ) + == 0 + ): if connect: - raise ValueError("must provide login/password, session_token or script_name/api_key") + raise ValueError( + "must provide login/password, session_token or script_name/api_key" + ) self.config = _Config(self) self.config.api_key = api_key @@ -643,19 +672,30 @@ def __init__(self, self.config.raw_http_proxy = http_proxy try: - self.config.rpc_attempt_interval = int(os.environ.get("SHOTGUN_API_RETRY_INTERVAL", 3000)) + self.config.rpc_attempt_interval = int( + os.environ.get("SHOTGUN_API_RETRY_INTERVAL", 3000) + ) except ValueError: retry_interval = os.environ.get("SHOTGUN_API_RETRY_INTERVAL", 3000) - raise ValueError("Invalid value '%s' found in environment variable " - "SHOTGUN_API_RETRY_INTERVAL, must be int." % retry_interval) + raise ValueError( + "Invalid value '%s' found in environment variable " + "SHOTGUN_API_RETRY_INTERVAL, must be int." % retry_interval + ) if self.config.rpc_attempt_interval < 0: - raise ValueError("Value of SHOTGUN_API_RETRY_INTERVAL must be positive, " - "got '%s'." % self.config.rpc_attempt_interval) - + raise ValueError( + "Value of SHOTGUN_API_RETRY_INTERVAL must be positive, " + "got '%s'." % self.config.rpc_attempt_interval + ) + global SHOTGUN_API_DISABLE_ENTITY_OPTIMIZATION - if os.environ.get("SHOTGUN_API_DISABLE_ENTITY_OPTIMIZATION", "0").strip().lower() == "1": + if ( + os.environ.get("SHOTGUN_API_DISABLE_ENTITY_OPTIMIZATION", "0") + .strip() + .lower() + == "1" + ): SHOTGUN_API_DISABLE_ENTITY_OPTIMIZATION = True - + self._connection = None self.__ca_certs = self._get_certs_file(ca_certs) @@ -672,8 +712,9 @@ def __init__(self, # the lowercase version of the credentials. auth, self.config.server = self._split_url(base_url) if auth: - auth = base64encode(sgutils.ensure_binary( - urllib.parse.unquote(auth))).decode("utf-8") + auth = base64encode( + sgutils.ensure_binary(urllib.parse.unquote(auth)) + ).decode("utf-8") self.config.authorization = "Basic " + auth.strip() # foo:bar@123.456.789.012:3456 @@ -682,8 +723,7 @@ def __init__(self, # there might be @ in the user's password. p = http_proxy.rsplit("@", 1) if len(p) > 1: - self.config.proxy_user, self.config.proxy_pass = \ - p[0].split(":", 1) + self.config.proxy_user, self.config.proxy_pass = p[0].split(":", 1) proxy_server = p[1] else: proxy_server = http_proxy @@ -693,18 +733,29 @@ def __init__(self, try: self.config.proxy_port = int(proxy_netloc_list[1]) except ValueError: - raise ValueError("Invalid http_proxy address '%s'. Valid " - "format is '123.456.789.012' or '123.456.789.012:3456'" - ". If no port is specified, a default of %d will be " - "used." % (http_proxy, self.config.proxy_port)) + raise ValueError( + "Invalid http_proxy address '%s'. Valid " + "format is '123.456.789.012' or '123.456.789.012:3456'" + ". If no port is specified, a default of %d will be " + "used." % (http_proxy, self.config.proxy_port) + ) # now populate self.config.proxy_handler if self.config.proxy_user and self.config.proxy_pass: - auth_string = "%s:%s@" % (self.config.proxy_user, self.config.proxy_pass) + auth_string = "%s:%s@" % ( + self.config.proxy_user, + self.config.proxy_pass, + ) else: auth_string = "" - proxy_addr = "http://%s%s:%d" % (auth_string, self.config.proxy_server, self.config.proxy_port) - self.config.proxy_handler = urllib.request.ProxyHandler({self.config.scheme: proxy_addr}) + proxy_addr = "http://%s%s:%d" % ( + auth_string, + self.config.proxy_server, + self.config.proxy_port, + ) + self.config.proxy_handler = urllib.request.ProxyHandler( + {self.config.scheme: proxy_addr} + ) if ensure_ascii: self._json_loads = self._json_loads_ascii @@ -750,7 +801,8 @@ def _split_url(self, base_url): else: auth, server = urllib.parse.splituser( - urllib.parse.urlsplit(base_url).netloc) + urllib.parse.urlsplit(base_url).netloc + ) return auth, server @@ -842,8 +894,17 @@ def info(self): """ return self._call_rpc("info", None, include_auth_params=False) - def find_one(self, entity_type, filters, fields=None, order=None, filter_operator=None, retired_only=False, - include_archived_projects=True, additional_filter_presets=None): + def find_one( + self, + entity_type, + filters, + fields=None, + order=None, + filter_operator=None, + retired_only=False, + include_archived_projects=True, + additional_filter_presets=None, + ): """ Shortcut for :meth:`~shotgun_api3.Shotgun.find` with ``limit=1`` so it returns a single result. @@ -897,16 +958,35 @@ def find_one(self, entity_type, filters, fields=None, order=None, filter_operato :rtype: dict """ - results = self.find(entity_type, filters, fields, order, filter_operator, 1, retired_only, - include_archived_projects=include_archived_projects, - additional_filter_presets=additional_filter_presets) + results = self.find( + entity_type, + filters, + fields, + order, + filter_operator, + 1, + retired_only, + include_archived_projects=include_archived_projects, + additional_filter_presets=additional_filter_presets, + ) if results: return results[0] return None - def find(self, entity_type, filters, fields=None, order=None, filter_operator=None, limit=0, - retired_only=False, page=0, include_archived_projects=True, additional_filter_presets=None): + def find( + self, + entity_type, + filters, + fields=None, + order=None, + filter_operator=None, + limit=0, + retired_only=False, + page=0, + include_archived_projects=True, + additional_filter_presets=None, + ): """ Find entities matching the given filters. @@ -959,7 +1039,7 @@ def find(self, entity_type, filters, fields=None, order=None, filter_operator=No Defaults to ``["id"]``. .. seealso:: :ref:`combining-related-queries` - + :param list order: Optional list of dictionaries defining how to order the results of the query. Each dictionary contains the ``field_name`` to order by and the ``direction`` to sort:: @@ -1016,8 +1096,10 @@ def find(self, entity_type, filters, fields=None, order=None, filter_operator=No filters = _translate_filters(filters, filter_operator) elif filter_operator: # TODO: Not sure if this test is correct, replicated from prev api - raise ShotgunError("Deprecated: Use of filter_operator for find() is not valid any more." - " See the documentation on find()") + raise ShotgunError( + "Deprecated: Use of filter_operator for find() is not valid any more." + " See the documentation on find()" + ) if not include_archived_projects: # This defaults to True on the server (no argument is sent) @@ -1027,13 +1109,15 @@ def find(self, entity_type, filters, fields=None, order=None, filter_operator=No if additional_filter_presets: self.server_caps.ensure_support_for_additional_filter_presets() - params = self._construct_read_parameters(entity_type, - fields, - filters, - retired_only, - order, - include_archived_projects, - additional_filter_presets) + params = self._construct_read_parameters( + entity_type, + fields, + filters, + retired_only, + order, + include_archived_projects, + additional_filter_presets, + ) if self.server_caps.ensure_return_image_urls_support(): params["api_return_image_urls"] = True @@ -1089,21 +1173,25 @@ def find(self, entity_type, filters, fields=None, order=None, filter_operator=No return self._parse_records(records) - def _construct_read_parameters(self, - entity_type, - fields, - filters, - retired_only, - order, - include_archived_projects, - additional_filter_presets): + def _construct_read_parameters( + self, + entity_type, + fields, + filters, + retired_only, + order, + include_archived_projects, + additional_filter_presets, + ): params = {} params["type"] = entity_type params["return_fields"] = fields or ["id"] params["filters"] = filters params["return_only"] = (retired_only and "retired") or "active" - params["paging"] = {"entities_per_page": self.config.records_per_page, - "current_page": 1} + params["paging"] = { + "entities_per_page": self.config.records_per_page, + "current_page": 1, + } if additional_filter_presets: params["additional_filter_presets"] = additional_filter_presets @@ -1119,10 +1207,9 @@ def _construct_read_parameters(self, # TODO: warn about deprecation of 'column' param name sort["field_name"] = sort["column"] sort.setdefault("direction", "asc") - sort_list.append({ - "field_name": sort["field_name"], - "direction": sort["direction"] - }) + sort_list.append( + {"field_name": sort["field_name"], "direction": sort["direction"]} + ) params["sorts"] = sort_list return params @@ -1132,7 +1219,7 @@ def _add_project_param(self, params, project_entity): params["project"] = project_entity return params - + def _translate_update_params( self, entity_type, entity_id, data, multi_entity_update_modes ): @@ -1155,13 +1242,15 @@ def optimize_field(field_dict): "fields": [optimize_field(field_dict) for field_dict in full_fields], } - def summarize(self, - entity_type, - filters, - summary_fields, - filter_operator=None, - grouping=None, - include_archived_projects=True): + def summarize( + self, + entity_type, + filters, + summary_fields, + filter_operator=None, + grouping=None, + include_archived_projects=True, + ): """ Summarize field data returned by a query. @@ -1349,9 +1438,7 @@ def summarize(self, # So we only need to check the server version if it is False self.server_caps.ensure_include_archived_projects() - params = {"type": entity_type, - "summaries": summary_fields, - "filters": filters} + params = {"type": entity_type, "summaries": summary_fields, "filters": filters} if include_archived_projects is False: # Defaults to True on the server, so only pass it if it's False @@ -1411,14 +1498,16 @@ def create(self, entity_type, data, return_fields=None): upload_filmstrip_image = None if "filmstrip_image" in data: if not self.server_caps.version or self.server_caps.version < (3, 1, 0): - raise ShotgunError("Filmstrip thumbnail support requires server version 3.1 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "Filmstrip thumbnail support requires server version 3.1 or " + "higher, server is %s" % (self.server_caps.version,) + ) upload_filmstrip_image = data.pop("filmstrip_image") params = { "type": entity_type, "fields": self._dict_to_list(data), - "return_fields": return_fields + "return_fields": return_fields, } record = self._call_rpc("create", params, first=True) @@ -1426,12 +1515,20 @@ def create(self, entity_type, data, return_fields=None): if upload_image: self.upload_thumbnail(entity_type, result["id"], upload_image) - image = self.find_one(entity_type, [["id", "is", result.get("id")]], fields=["image"]) + image = self.find_one( + entity_type, [["id", "is", result.get("id")]], fields=["image"] + ) result["image"] = image.get("image") if upload_filmstrip_image: - self.upload_filmstrip_thumbnail(entity_type, result["id"], upload_filmstrip_image) - filmstrip = self.find_one(entity_type, [["id", "is", result.get("id")]], fields=["filmstrip_image"]) + self.upload_filmstrip_thumbnail( + entity_type, result["id"], upload_filmstrip_image + ) + filmstrip = self.find_one( + entity_type, + [["id", "is", result.get("id")]], + fields=["filmstrip_image"], + ) result["filmstrip_image"] = filmstrip.get("filmstrip_image") return result @@ -1480,12 +1577,16 @@ def update(self, entity_type, entity_id, data, multi_entity_update_modes=None): upload_filmstrip_image = None if "filmstrip_image" in data: if not self.server_caps.version or self.server_caps.version < (3, 1, 0): - raise ShotgunError("Filmstrip thumbnail support requires server version 3.1 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "Filmstrip thumbnail support requires server version 3.1 or " + "higher, server is %s" % (self.server_caps.version,) + ) upload_filmstrip_image = data.pop("filmstrip_image") if data: - params = self._translate_update_params(entity_type, entity_id, data, multi_entity_update_modes) + params = self._translate_update_params( + entity_type, entity_id, data, multi_entity_update_modes + ) record = self._call_rpc("update", params) result = self._parse_records(record)[0] else: @@ -1493,12 +1594,20 @@ def update(self, entity_type, entity_id, data, multi_entity_update_modes=None): if upload_image: self.upload_thumbnail(entity_type, entity_id, upload_image) - image = self.find_one(entity_type, [["id", "is", result.get("id")]], fields=["image"]) + image = self.find_one( + entity_type, [["id", "is", result.get("id")]], fields=["image"] + ) result["image"] = image.get("image") if upload_filmstrip_image: - self.upload_filmstrip_thumbnail(entity_type, result["id"], upload_filmstrip_image) - filmstrip = self.find_one(entity_type, [["id", "is", result.get("id")]], fields=["filmstrip_image"]) + self.upload_filmstrip_thumbnail( + entity_type, result["id"], upload_filmstrip_image + ) + filmstrip = self.find_one( + entity_type, + [["id", "is", result.get("id")]], + fields=["filmstrip_image"], + ) result["filmstrip_image"] = filmstrip.get("filmstrip_image") return result @@ -1521,12 +1630,9 @@ def delete(self, entity_type, entity_id): entity was already deleted). :rtype: bool :raises: :class:`Fault` if entity does not exist (deleted or not). - """ + """ - params = { - "type": entity_type, - "id": entity_id - } + params = {"type": entity_type, "id": entity_id} return self._call_rpc("delete", params) @@ -1544,10 +1650,7 @@ def revive(self, entity_type, entity_id): :rtype: bool """ - params = { - "type": entity_type, - "id": entity_id - } + params = {"type": entity_type, "id": entity_id} return self._call_rpc("revive", params) @@ -1612,7 +1715,9 @@ def batch(self, requests): """ if not isinstance(requests, list): - raise ShotgunError("batch() expects a list. Instead was sent a %s" % type(requests)) + raise ShotgunError( + "batch() expects a list. Instead was sent a %s" % type(requests) + ) # If we have no requests, just return an empty list immediately. # Nothing to process means nothing to get results of. @@ -1624,39 +1729,42 @@ def batch(self, requests): def _required_keys(message, required_keys, data): missing = set(required_keys) - set(data.keys()) if missing: - raise ShotgunError("%s missing required key: %s. " - "Value was: %s." % (message, ", ".join(missing), data)) + raise ShotgunError( + "%s missing required key: %s. " + "Value was: %s." % (message, ", ".join(missing), data) + ) for req in requests: - _required_keys("Batched request", - ["request_type", "entity_type"], - req) - request_params = {"request_type": req["request_type"], "type": req["entity_type"]} + _required_keys("Batched request", ["request_type", "entity_type"], req) + request_params = { + "request_type": req["request_type"], + "type": req["entity_type"], + } if req["request_type"] == "create": _required_keys("Batched create request", ["data"], req) request_params["fields"] = self._dict_to_list(req["data"]) - request_params["return_fields"] = req.get("return_fields") or["id"] + request_params["return_fields"] = req.get("return_fields") or ["id"] elif req["request_type"] == "update": - _required_keys("Batched update request", - ["entity_id", "data"], - req) + _required_keys("Batched update request", ["entity_id", "data"], req) request_params["id"] = req["entity_id"] request_params["fields"] = self._dict_to_list( req["data"], extra_data=self._dict_to_extra_data( - req.get("multi_entity_update_modes"), - "multi_entity_update_mode" - ) + req.get("multi_entity_update_modes"), "multi_entity_update_mode" + ), ) if "multi_entity_update_mode" in req: - request_params["multi_entity_update_mode"] = req["multi_entity_update_mode"] + request_params["multi_entity_update_mode"] = req[ + "multi_entity_update_mode" + ] elif req["request_type"] == "delete": _required_keys("Batched delete request", ["entity_id"], req) request_params["id"] = req["entity_id"] else: - raise ShotgunError("Invalid request_type '%s' for batch" % ( - req["request_type"])) + raise ShotgunError( + "Invalid request_type '%s' for batch" % (req["request_type"]) + ) calls.append(request_params) records = self._call_rpc("batch", calls) return self._parse_records(records) @@ -1714,23 +1822,31 @@ def work_schedule_read(self, start_date, end_date, project=None, user=None): """ if not self.server_caps.version or self.server_caps.version < (3, 2, 0): - raise ShotgunError("Work schedule support requires server version 3.2 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "Work schedule support requires server version 3.2 or " + "higher, server is %s" % (self.server_caps.version,) + ) if not isinstance(start_date, str) or not isinstance(end_date, str): - raise ShotgunError("The start_date and end_date arguments must be strings in YYYY-MM-DD format") + raise ShotgunError( + "The start_date and end_date arguments must be strings in YYYY-MM-DD format" + ) params = dict( - start_date=start_date, - end_date=end_date, - project=project, - user=user + start_date=start_date, end_date=end_date, project=project, user=user ) return self._call_rpc("work_schedule_read", params) - def work_schedule_update(self, date, working, description=None, project=None, user=None, - recalculate_field=None): + def work_schedule_update( + self, + date, + working, + description=None, + project=None, + user=None, + recalculate_field=None, + ): """ Update the work schedule for a given date. @@ -1765,8 +1881,10 @@ def work_schedule_update(self, date, working, description=None, project=None, us """ if not self.server_caps.version or self.server_caps.version < (3, 2, 0): - raise ShotgunError("Work schedule support requires server version 3.2 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "Work schedule support requires server version 3.2 or " + "higher, server is %s" % (self.server_caps.version,) + ) if not isinstance(date, str): raise ShotgunError("The date argument must be string in YYYY-MM-DD format") @@ -1777,7 +1895,7 @@ def work_schedule_update(self, date, working, description=None, project=None, us description=description, project=project, user=user, - recalculate_field=recalculate_field + recalculate_field=recalculate_field, ) return self._call_rpc("work_schedule_update", params) @@ -1801,13 +1919,12 @@ def follow(self, user, entity): """ if not self.server_caps.version or self.server_caps.version < (5, 1, 22): - raise ShotgunError("Follow support requires server version 5.2 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "Follow support requires server version 5.2 or " + "higher, server is %s" % (self.server_caps.version,) + ) - params = dict( - user=user, - entity=entity - ) + params = dict(user=user, entity=entity) return self._call_rpc("follow", params) @@ -1829,13 +1946,12 @@ def unfollow(self, user, entity): """ if not self.server_caps.version or self.server_caps.version < (5, 1, 22): - raise ShotgunError("Follow support requires server version 5.2 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "Follow support requires server version 5.2 or " + "higher, server is %s" % (self.server_caps.version,) + ) - params = dict( - user=user, - entity=entity - ) + params = dict(user=user, entity=entity) return self._call_rpc("unfollow", params) @@ -1858,12 +1974,12 @@ def followers(self, entity): """ if not self.server_caps.version or self.server_caps.version < (5, 1, 22): - raise ShotgunError("Follow support requires server version 5.2 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "Follow support requires server version 5.2 or " + "higher, server is %s" % (self.server_caps.version,) + ) - params = dict( - entity=entity - ) + params = dict(entity=entity) return self._call_rpc("followers", params) @@ -1890,9 +2006,7 @@ def following(self, user, project=None, entity_type=None): self.server_caps.ensure_user_following_support() - params = { - "user": user - } + params = {"user": user} if project: params["project"] = project if entity_type: @@ -2080,7 +2194,9 @@ def schema_field_read(self, entity_type, field_name=None, project_entity=None): return self._call_rpc("schema_field_read", params) - def schema_field_create(self, entity_type, data_type, display_name, properties=None): + def schema_field_create( + self, entity_type, data_type, display_name, properties=None + ): """ Create a field for the specified entity type. @@ -2109,15 +2225,17 @@ def schema_field_create(self, entity_type, data_type, display_name, properties=N params = { "type": entity_type, "data_type": data_type, - "properties": [ - {"property_name": "name", "value": display_name} - ] + "properties": [{"property_name": "name", "value": display_name}], } - params["properties"].extend(self._dict_to_list(properties, key_name="property_name", value_name="value")) + params["properties"].extend( + self._dict_to_list(properties, key_name="property_name", value_name="value") + ) return self._call_rpc("schema_field_create", params) - def schema_field_update(self, entity_type, field_name, properties, project_entity=None): + def schema_field_update( + self, entity_type, field_name, properties, project_entity=None + ): """ Update the properties for the specified field on an entity. @@ -2154,7 +2272,7 @@ def schema_field_update(self, entity_type, field_name, properties, project_entit "properties": [ {"property_name": k, "value": v} for k, v in six.iteritems((properties or {})) - ] + ], } params = self._add_project_param(params, project_entity) return self._call_rpc("schema_field_update", params) @@ -2172,10 +2290,7 @@ def schema_field_delete(self, entity_type, field_name): :rtype: bool """ - params = { - "type": entity_type, - "field_name": field_name - } + params = {"type": entity_type, "field_name": field_name} return self._call_rpc("schema_field_delete", params) @@ -2209,9 +2324,11 @@ def reset_user_agent(self): if self.config.no_ssl_validation: validation_str = "no-validate" - self._user_agents = ["shotgun-json (%s)" % __version__, - "Python %s (%s)" % (self.client_caps.py_version, ua_platform), - "ssl %s (%s)" % (self.client_caps.ssl_version, validation_str)] + self._user_agents = [ + "shotgun-json (%s)" % __version__, + "Python %s (%s)" % (self.client_caps.py_version, ua_platform), + "ssl %s (%s)" % (self.client_caps.ssl_version, validation_str), + ] def set_session_uuid(self, session_uuid): """ @@ -2229,8 +2346,14 @@ def set_session_uuid(self, session_uuid): self.config.session_uuid = session_uuid return - def share_thumbnail(self, entities, thumbnail_path=None, source_entity=None, - filmstrip_thumbnail=False, **kwargs): + def share_thumbnail( + self, + entities, + thumbnail_path=None, + source_entity=None, + filmstrip_thumbnail=False, + **kwargs, + ): """ Associate a thumbnail with more than one Shotgun entity. @@ -2246,7 +2369,7 @@ def share_thumbnail(self, entities, thumbnail_path=None, source_entity=None, .. note:: When sharing a filmstrip thumbnail, it is required to have a static thumbnail in place before the filmstrip will be displayed in the Shotgun web UI. - If the :ref:`thumbnail is still processing and is using a placeholder + If the :ref:`thumbnail is still processing and is using a placeholder `, this method will error. Simple use case: @@ -2273,48 +2396,58 @@ def share_thumbnail(self, entities, thumbnail_path=None, source_entity=None, share the static thumbnail. Defaults to ``False``. :returns: ``id`` of the Attachment entity representing the source thumbnail that is shared. :rtype: int - :raises: :class:`ShotgunError` if not supported by server version or improperly called, + :raises: :class:`ShotgunError` if not supported by server version or improperly called, or :class:`ShotgunThumbnailNotReady` if thumbnail is still pending. """ if not self.server_caps.version or self.server_caps.version < (4, 0, 0): - raise ShotgunError("Thumbnail sharing support requires server " - "version 4.0 or higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "Thumbnail sharing support requires server " + "version 4.0 or higher, server is %s" % (self.server_caps.version,) + ) if not isinstance(entities, list) or len(entities) == 0: - raise ShotgunError("'entities' parameter must be a list of entity " - "hashes and may not be empty") + raise ShotgunError( + "'entities' parameter must be a list of entity " + "hashes and may not be empty" + ) for e in entities: if not isinstance(e, dict) or "id" not in e or "type" not in e: - raise ShotgunError("'entities' parameter must be a list of " - "entity hashes with at least 'type' and 'id' keys.\nInvalid " - "entity: %s" % e) + raise ShotgunError( + "'entities' parameter must be a list of " + "entity hashes with at least 'type' and 'id' keys.\nInvalid " + "entity: %s" % e + ) - if (not thumbnail_path and not source_entity) or (thumbnail_path and source_entity): - raise ShotgunError("You must supply either thumbnail_path OR source_entity.") + if (not thumbnail_path and not source_entity) or ( + thumbnail_path and source_entity + ): + raise ShotgunError( + "You must supply either thumbnail_path OR source_entity." + ) # upload thumbnail if thumbnail_path: source_entity = entities.pop(0) if filmstrip_thumbnail: thumb_id = self.upload_filmstrip_thumbnail( - source_entity["type"], - source_entity["id"], - thumbnail_path, - **kwargs + source_entity["type"], source_entity["id"], thumbnail_path, **kwargs ) else: thumb_id = self.upload_thumbnail( - source_entity["type"], - source_entity["id"], - thumbnail_path, - **kwargs + source_entity["type"], source_entity["id"], thumbnail_path, **kwargs ) else: - if not isinstance(source_entity, dict) or "id" not in source_entity or "type" not in source_entity: - raise ShotgunError("'source_entity' parameter must be a dict " - "with at least 'type' and 'id' keys.\nGot: %s (%s)" - % (source_entity, type(source_entity))) + if ( + not isinstance(source_entity, dict) + or "id" not in source_entity + or "type" not in source_entity + ): + raise ShotgunError( + "'source_entity' parameter must be a dict " + "with at least 'type' and 'id' keys.\nGot: %s (%s)" + % (source_entity, type(source_entity)) + ) # only 1 entity in list and we already uploaded the thumbnail to it if len(entities) == 0: @@ -2333,8 +2466,16 @@ def share_thumbnail(self, entities, thumbnail_path=None, source_entity=None, "filmstrip_thumbnail": filmstrip_thumbnail, } - url = urllib.parse.urlunparse((self.config.scheme, self.config.server, - "/upload/share_thumbnail", None, None, None)) + url = urllib.parse.urlunparse( + ( + self.config.scheme, + self.config.server, + "/upload/share_thumbnail", + None, + None, + None, + ) + ) result = self._send_form(url, params) @@ -2377,7 +2518,9 @@ def upload_thumbnail(self, entity_type, entity_id, path, **kwargs): :param str path: Full path to the thumbnail file on disk. :returns: Id of the new attachment """ - return self.upload(entity_type, entity_id, path, field_name="thumb_image", **kwargs) + return self.upload( + entity_type, entity_id, path, field_name="thumb_image", **kwargs + ) def upload_filmstrip_thumbnail(self, entity_type, entity_id, path, **kwargs): """ @@ -2419,13 +2562,24 @@ def upload_filmstrip_thumbnail(self, entity_type, entity_id, path, **kwargs): :rtype: int """ if not self.server_caps.version or self.server_caps.version < (3, 1, 0): - raise ShotgunError("Filmstrip thumbnail support requires server version 3.1 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "Filmstrip thumbnail support requires server version 3.1 or " + "higher, server is %s" % (self.server_caps.version,) + ) - return self.upload(entity_type, entity_id, path, field_name="filmstrip_thumb_image", **kwargs) + return self.upload( + entity_type, entity_id, path, field_name="filmstrip_thumb_image", **kwargs + ) - def upload(self, entity_type, entity_id, path, field_name=None, display_name=None, - tag_list=None): + def upload( + self, + entity_type, + entity_id, + path, + field_name=None, + display_name=None, + tag_list=None, + ): """ Upload a file to the specified entity. @@ -2434,7 +2588,7 @@ def upload(self, entity_type, entity_id, path, field_name=None, display_name=Non assign tags to the Attachment. .. note:: - Make sure to have retries for file uploads. Failures when uploading will occasionally happen. + Make sure to have retries for file uploads. Failures when uploading will occasionally happen. When it does, immediately retrying to upload usually works >>> mov_file = '/data/show/ne2/100_110/anim/01.mlk-02b.mov' @@ -2477,19 +2631,45 @@ def upload(self, entity_type, entity_id, path, field_name=None, display_name=Non if os.path.getsize(path) == 0: raise ShotgunError("Path cannot be an empty file: '%s'" % path) - is_thumbnail = (field_name in ["thumb_image", "filmstrip_thumb_image", "image", - "filmstrip_image"]) + is_thumbnail = field_name in [ + "thumb_image", + "filmstrip_thumb_image", + "image", + "filmstrip_image", + ] # Supported types can be directly uploaded to Cloud storage if self._requires_direct_s3_upload(entity_type, field_name): - return self._upload_to_storage(entity_type, entity_id, path, field_name, display_name, - tag_list, is_thumbnail) + return self._upload_to_storage( + entity_type, + entity_id, + path, + field_name, + display_name, + tag_list, + is_thumbnail, + ) else: - return self._upload_to_sg(entity_type, entity_id, path, field_name, display_name, - tag_list, is_thumbnail) + return self._upload_to_sg( + entity_type, + entity_id, + path, + field_name, + display_name, + tag_list, + is_thumbnail, + ) - def _upload_to_storage(self, entity_type, entity_id, path, field_name, display_name, - tag_list, is_thumbnail): + def _upload_to_storage( + self, + entity_type, + entity_id, + path, + field_name, + display_name, + tag_list, + is_thumbnail, + ): """ Internal function to upload a file to the Cloud storage and link it to the specified entity. @@ -2509,9 +2689,11 @@ def _upload_to_storage(self, entity_type, entity_id, path, field_name, display_n # Step 1: get the upload url - is_multipart_upload = (os.path.getsize(path) > self._MULTIPART_UPLOAD_CHUNK_SIZE) + is_multipart_upload = os.path.getsize(path) > self._MULTIPART_UPLOAD_CHUNK_SIZE - upload_info = self._get_attachment_upload_info(is_thumbnail, filename, is_multipart_upload) + upload_info = self._get_attachment_upload_info( + is_thumbnail, filename, is_multipart_upload + ) # Step 2: upload the file # We upload large files in multiple parts because it is more robust @@ -2523,13 +2705,21 @@ def _upload_to_storage(self, entity_type, entity_id, path, field_name, display_n # Step 3: create the attachment - url = urllib.parse.urlunparse((self.config.scheme, self.config.server, - "/upload/api_link_file", None, None, None)) + url = urllib.parse.urlunparse( + ( + self.config.scheme, + self.config.server, + "/upload/api_link_file", + None, + None, + None, + ) + ) params = { "entity_type": entity_type, "entity_id": entity_id, - "upload_link_info": upload_info["upload_info"] + "upload_link_info": upload_info["upload_info"], } params.update(self._auth_params()) @@ -2550,17 +2740,26 @@ def _upload_to_storage(self, entity_type, entity_id, path, field_name, display_n result = self._send_form(url, params) if not result.startswith("1"): - raise ShotgunError("Could not upload file successfully, but " - "not sure why.\nPath: %s\nUrl: %s\nError: %s" - % (path, url, result)) + raise ShotgunError( + "Could not upload file successfully, but " + "not sure why.\nPath: %s\nUrl: %s\nError: %s" % (path, url, result) + ) LOG.debug("Attachment linked to content on Cloud storage") attachment_id = int(result.split(":", 2)[1].split("\n", 1)[0]) return attachment_id - def _upload_to_sg(self, entity_type, entity_id, path, field_name, display_name, - tag_list, is_thumbnail): + def _upload_to_sg( + self, + entity_type, + entity_id, + path, + field_name, + display_name, + tag_list, + is_thumbnail, + ): """ Internal function to upload a file to Shotgun and link it to the specified entity. @@ -2585,14 +2784,30 @@ def _upload_to_sg(self, entity_type, entity_id, path, field_name, display_name, params.update(self._auth_params()) if is_thumbnail: - url = urllib.parse.urlunparse((self.config.scheme, self.config.server, - "/upload/publish_thumbnail", None, None, None)) + url = urllib.parse.urlunparse( + ( + self.config.scheme, + self.config.server, + "/upload/publish_thumbnail", + None, + None, + None, + ) + ) params["thumb_image"] = open(path, "rb") if field_name == "filmstrip_thumb_image" or field_name == "filmstrip_image": params["filmstrip"] = True else: - url = urllib.parse.urlunparse((self.config.scheme, self.config.server, - "/upload/upload_file", None, None, None)) + url = urllib.parse.urlunparse( + ( + self.config.scheme, + self.config.server, + "/upload/upload_file", + None, + None, + None, + ) + ) if display_name is None: display_name = os.path.basename(path) # we allow linking to nothing for generic reference use cases @@ -2608,9 +2823,10 @@ def _upload_to_sg(self, entity_type, entity_id, path, field_name, display_name, result = self._send_form(url, params) if not result.startswith("1"): - raise ShotgunError("Could not upload file successfully, but " - "not sure why.\nPath: %s\nUrl: %s\nError: %s" - % (path, url, result)) + raise ShotgunError( + "Could not upload file successfully, but " + "not sure why.\nPath: %s\nUrl: %s\nError: %s" % (path, url, result) + ) attachment_id = int(result.split(":", 2)[1].split("\n", 1)[0]) return attachment_id @@ -2633,21 +2849,22 @@ def _get_attachment_upload_info(self, is_thumbnail, filename, is_multipart_uploa else: upload_type = "Attachment" - params = { - "upload_type": upload_type, - "filename": filename - } + params = {"upload_type": upload_type, "filename": filename} params["multipart_upload"] = is_multipart_upload upload_url = "/upload/api_get_upload_link_info" - url = urllib.parse.urlunparse((self.config.scheme, self.config.server, upload_url, None, None, None)) + url = urllib.parse.urlunparse( + (self.config.scheme, self.config.server, upload_url, None, None, None) + ) upload_info = self._send_form(url, params) if not upload_info.startswith("1"): - raise ShotgunError("Could not get upload_url but " - "not sure why.\nPath: %s\nUrl: %s\nError: %s" - % (filename, url, upload_info)) + raise ShotgunError( + "Could not get upload_url but " + "not sure why.\nPath: %s\nUrl: %s\nError: %s" + % (filename, url, upload_info) + ) LOG.debug("Completed rpc call to %s" % (upload_url)) @@ -2658,7 +2875,7 @@ def _get_attachment_upload_info(self, is_thumbnail, filename, is_multipart_uploa "timestamp": upload_info_parts[2], "upload_type": upload_info_parts[3], "upload_id": upload_info_parts[4], - "upload_info": upload_info + "upload_info": upload_info, } def download_attachment(self, attachment=False, file_path=None, attachment_id=None): @@ -2702,16 +2919,19 @@ def download_attachment(self, attachment=False, file_path=None, attachment_id=No if type(attachment_id) == int: attachment = attachment_id else: - raise TypeError("Missing parameter 'attachment'. Expected a " - "dict, int, NoneType value or" - "an int for parameter attachment_id") + raise TypeError( + "Missing parameter 'attachment'. Expected a " + "dict, int, NoneType value or" + "an int for parameter attachment_id" + ) # write to disk if file_path: try: fp = open(file_path, "wb") except IOError as e: - raise IOError("Unable to write Attachment to disk using " - "file_path. %s" % e) + raise IOError( + "Unable to write Attachment to disk using " "file_path. %s" % e + ) url = self.get_attachment_download_url(attachment) if url is None: @@ -2742,7 +2962,10 @@ def download_attachment(self, attachment=False, file_path=None, attachment_id=No err += "\nAttachment may not exist or is a local file?" elif e.code == 403: # Only parse the body if it is an Amazon S3 url. - if url.find("s3.amazonaws.com") != -1 and e.headers["content-type"] == "application/xml": + if ( + url.find("s3.amazonaws.com") != -1 + and e.headers["content-type"] == "application/xml" + ): body = [sgutils.ensure_text(line) for line in e.readlines()] if body: xml = "".join(body) @@ -2778,8 +3001,24 @@ def get_auth_cookie_handler(self): """ sid = self.get_session_token() cj = http_cookiejar.LWPCookieJar() - c = http_cookiejar.Cookie("0", "_session_id", sid, None, False, self.config.server, False, - False, "/", True, False, None, True, None, None, {}) + c = http_cookiejar.Cookie( + "0", + "_session_id", + sid, + None, + False, + self.config.server, + False, + False, + "/", + True, + False, + None, + True, + None, + None, + {}, + ) cj.set_cookie(c) return urllib.request.HTTPCookieProcessor(cj) @@ -2811,20 +3050,34 @@ def get_attachment_download_url(self, attachment): try: url = attachment["url"] except KeyError: - if ("id" in attachment and "type" in attachment and attachment["type"] == "Attachment"): + if ( + "id" in attachment + and "type" in attachment + and attachment["type"] == "Attachment" + ): attachment_id = attachment["id"] else: raise ValueError("Missing 'url' key in Attachment dict") elif attachment is None: url = None else: - raise TypeError("Unable to determine download url. Expected " - "dict, int, or NoneType. Instead got %s" % type(attachment)) + raise TypeError( + "Unable to determine download url. Expected " + "dict, int, or NoneType. Instead got %s" % type(attachment) + ) if attachment_id: - url = urllib.parse.urlunparse((self.config.scheme, self.config.server, - "/file_serve/attachment/%s" % urllib.parse.quote(str(attachment_id)), - None, None, None)) + url = urllib.parse.urlunparse( + ( + self.config.scheme, + self.config.server, + "/file_serve/attachment/%s" + % urllib.parse.quote(str(attachment_id)), + None, + None, + None, + ) + ) return url def authenticate_human_user(self, user_login, user_password, auth_token=None): @@ -2862,9 +3115,13 @@ def authenticate_human_user(self, user_login, user_password, auth_token=None): self.config.auth_token = auth_token try: - data = self.find_one("HumanUser", [["sg_status_list", "is", "act"], - ["login", "is", user_login]], - ["id", "login"], "", "all") + data = self.find_one( + "HumanUser", + [["sg_status_list", "is", "act"], ["login", "is", user_login]], + ["id", "login"], + "", + "all", + ) # Set back to default - There finally and except cannot be used together in python2.4 self.config.user_login = original_login self.config.user_password = original_password @@ -2902,18 +3159,26 @@ def update_project_last_accessed(self, project, user=None): value from the current instance will be used instead. """ if self.server_caps.version and self.server_caps.version < (5, 3, 20): - raise ShotgunError("update_project_last_accessed requires server version 5.3.20 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "update_project_last_accessed requires server version 5.3.20 or " + "higher, server is %s" % (self.server_caps.version,) + ) if not user: # Try to use sudo as user if present if self.config.sudo_as_login: - user = self.find_one("HumanUser", [["login", "is", self.config.sudo_as_login]]) + user = self.find_one( + "HumanUser", [["login", "is", self.config.sudo_as_login]] + ) # Try to use login if present if self.config.user_login: - user = self.find_one("HumanUser", [["login", "is", self.config.user_login]]) + user = self.find_one( + "HumanUser", [["login", "is", self.config.user_login]] + ) - params = {"project_id": project["id"], } + params = { + "project_id": project["id"], + } if user: params["user_id"] = user["id"] @@ -2979,8 +3244,10 @@ def note_thread_read(self, note_id, entity_fields=None): """ if self.server_caps.version and self.server_caps.version < (6, 2, 0): - raise ShotgunError("note_thread requires server version 6.2.0 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "note_thread requires server version 6.2.0 or " + "higher, server is %s" % (self.server_caps.version,) + ) entity_fields = entity_fields or {} @@ -3050,8 +3317,10 @@ def text_search(self, text, entity_types, project_ids=None, limit=None): :rtype: dict """ if self.server_caps.version and self.server_caps.version < (6, 2, 0): - raise ShotgunError("auto_complete requires server version 6.2.0 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "auto_complete requires server version 6.2.0 or " + "higher, server is %s" % (self.server_caps.version,) + ) # convert entity_types structure into the form # that the API endpoint expects @@ -3059,28 +3328,39 @@ def text_search(self, text, entity_types, project_ids=None, limit=None): raise ValueError("entity_types parameter must be a dictionary") api_entity_types = {} - for (entity_type, filter_list) in six.iteritems(entity_types): + for entity_type, filter_list in six.iteritems(entity_types): if isinstance(filter_list, (list, tuple)): resolved_filters = _translate_filters(filter_list, filter_operator=None) api_entity_types[entity_type] = resolved_filters else: - raise ValueError("value of entity_types['%s'] must " - "be a list or tuple." % entity_type) + raise ValueError( + "value of entity_types['%s'] must " + "be a list or tuple." % entity_type + ) project_ids = project_ids or [] - params = {"text": text, - "entity_types": api_entity_types, - "project_ids": project_ids, - "max_results": limit} + params = { + "text": text, + "entity_types": api_entity_types, + "project_ids": project_ids, + "max_results": limit, + } record = self._call_rpc("query_display_name_cache", params) result = self._parse_records(record)[0] return result - def activity_stream_read(self, entity_type, entity_id, entity_fields=None, min_id=None, - max_id=None, limit=None): + def activity_stream_read( + self, + entity_type, + entity_id, + entity_fields=None, + min_id=None, + max_id=None, + limit=None, + ): """ Retrieve activity stream data from Shotgun. @@ -3146,8 +3426,10 @@ def activity_stream_read(self, entity_type, entity_id, entity_fields=None, min_i :rtype: dict """ if self.server_caps.version and self.server_caps.version < (6, 2, 0): - raise ShotgunError("activity_stream requires server version 6.2.0 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "activity_stream requires server version 6.2.0 or " + "higher, server is %s" % (self.server_caps.version,) + ) # set up parameters to send to server. entity_fields = entity_fields or {} @@ -3155,12 +3437,14 @@ def activity_stream_read(self, entity_type, entity_id, entity_fields=None, min_i if not isinstance(entity_fields, dict): raise ValueError("entity_fields parameter must be a dictionary") - params = {"type": entity_type, - "id": entity_id, - "max_id": max_id, - "min_id": min_id, - "limit": limit, - "entity_fields": entity_fields} + params = { + "type": entity_type, + "id": entity_id, + "max_id": max_id, + "min_id": min_id, + "limit": limit, + "entity_fields": entity_fields, + } record = self._call_rpc("activity_stream", params) result = self._parse_records(record)[0] @@ -3182,8 +3466,8 @@ def nav_expand(self, path, seed_entity_field=None, entity_fields=None): { "path": path, "seed_entity_field": seed_entity_field, - "entity_fields": entity_fields - } + "entity_fields": entity_fields, + }, ) def nav_search_string(self, root_path, search_string, seed_entity_field=None): @@ -3201,8 +3485,8 @@ def nav_search_string(self, root_path, search_string, seed_entity_field=None): { "root_path": root_path, "seed_entity_field": seed_entity_field, - "search_criteria": {"search_string": search_string} - } + "search_criteria": {"search_string": search_string}, + }, ) def nav_search_entity(self, root_path, entity, seed_entity_field=None): @@ -3221,8 +3505,8 @@ def nav_search_entity(self, root_path, entity, seed_entity_field=None): { "root_path": root_path, "seed_entity_field": seed_entity_field, - "search_criteria": {"entity": entity} - } + "search_criteria": {"entity": entity}, + }, ) def get_session_token(self): @@ -3263,8 +3547,10 @@ def preferences_read(self, prefs=None): :rtype: dict """ if self.server_caps.version and self.server_caps.version < (7, 10, 0): - raise ShotgunError("preferences_read requires server version 7.10.0 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "preferences_read requires server version 7.10.0 or " + "higher, server is %s" % (self.server_caps.version,) + ) prefs = prefs or [] @@ -3297,10 +3583,7 @@ def user_subscriptions_create(self, users): :rtype: bool """ - response = self._call_rpc( - "user_subscriptions_create", - {"users": users} - ) + response = self._call_rpc("user_subscriptions_create", {"users": users}) if not isinstance(response, dict): return False @@ -3390,9 +3673,14 @@ def _turn_off_ssl_validation(self): self.config.no_ssl_validation = True NO_SSL_VALIDATION = True # reset ssl-validation in user-agents - self._user_agents = ["ssl %s (no-validate)" % self.client_caps.ssl_version - if ua.startswith("ssl ") else ua - for ua in self._user_agents] + self._user_agents = [ + ( + "ssl %s (no-validate)" % self.client_caps.ssl_version + if ua.startswith("ssl ") + else ua + ) + for ua in self._user_agents + ] # Deprecated methods from old wrapper def schema(self, entity_type): @@ -3400,7 +3688,9 @@ def schema(self, entity_type): .. deprecated:: 3.0.0 Use :meth:`~shotgun_api3.Shotgun.schema_field_read` instead. """ - raise ShotgunError("Deprecated: use schema_field_read('%s') instead" % entity_type) + raise ShotgunError( + "Deprecated: use schema_field_read('%s') instead" % entity_type + ) def entity_types(self): """ @@ -3408,6 +3698,7 @@ def entity_types(self): Use :meth:`~shotgun_api3.Shotgun.schema_entity_read` instead. """ raise ShotgunError("Deprecated: use schema_entity_read() instead") + # ======================================================================== # RPC Functions @@ -3416,16 +3707,17 @@ def _call_rpc(self, method, params, include_auth_params=True, first=False): Call the specified method on the Shotgun Server sending the supplied payload. """ - LOG.debug("Starting rpc call to %s with params %s" % ( - method, params)) + LOG.debug("Starting rpc call to %s with params %s" % (method, params)) params = self._transform_outbound(params) - payload = self._build_payload(method, params, include_auth_params=include_auth_params) + payload = self._build_payload( + method, params, include_auth_params=include_auth_params + ) encoded_payload = self._encode_payload(payload) req_headers = { "content-type": "application/json; charset=utf-8", - "connection": "keep-alive" + "connection": "keep-alive", } if self.config.localized is True: @@ -3497,8 +3789,10 @@ def _auth_params(self): # Authenticate using session_id elif self.config.session_token: if self.server_caps.version and self.server_caps.version < (5, 3, 0): - raise ShotgunError("Session token based authentication requires server version " - "5.3.0 or higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "Session token based authentication requires server version " + "5.3.0 or higher, server is %s" % (self.server_caps.version,) + ) auth_params = {"session_token": str(self.config.session_token)} @@ -3516,8 +3810,10 @@ def _auth_params(self): # Make sure sudo_as_login is supported by server version if self.config.sudo_as_login: if self.server_caps.version and self.server_caps.version < (5, 3, 12): - raise ShotgunError("Option 'sudo_as_login' requires server version 5.3.12 or " - "higher, server is %s" % (self.server_caps.version,)) + raise ShotgunError( + "Option 'sudo_as_login' requires server version 5.3.12 or " + "higher, server is %s" % (self.server_caps.version,) + ) auth_params["sudo_as_login"] = self.config.sudo_as_login if self.config.extra_auth_params: @@ -3552,10 +3848,7 @@ def _build_payload(self, method, params, include_auth_params=True): if params: call_params.append(params) - return { - "method_name": method, - "params": call_params - } + return {"method_name": method, "params": call_params} def _encode_payload(self, payload): """ @@ -3588,7 +3881,7 @@ def _make_call(self, verb, path, body, headers): max_rpc_attempts = self.config.max_rpc_attempts rpc_attempt_interval = self.config.rpc_attempt_interval / 1000.0 - while (attempt < max_rpc_attempts): + while attempt < max_rpc_attempts: attempt += 1 try: return self._http_request(verb, path, body, req_headers) @@ -3624,15 +3917,19 @@ def _make_call(self, verb, path, body, headers): # unknown message digest algorithm # # Any other exceptions simply get raised. - if "unknown message digest algorithm" not in str(e) or \ - "SHOTGUN_FORCE_CERTIFICATE_VALIDATION" in os.environ: + if ( + "unknown message digest algorithm" not in str(e) + or "SHOTGUN_FORCE_CERTIFICATE_VALIDATION" in os.environ + ): raise if self.config.no_ssl_validation is False: - LOG.warning("SSL Error: this Python installation is incompatible with " - "certificates signed with SHA-2. Disabling certificate validation. " - "For more information, see https://www.shotgridsoftware.com/blog/" - "important-ssl-certificate-renewal-and-sha-2/") + LOG.warning( + "SSL Error: this Python installation is incompatible with " + "certificates signed with SHA-2. Disabling certificate validation. " + "For more information, see https://www.shotgridsoftware.com/blog/" + "important-ssl-certificate-renewal-and-sha-2/" + ) self._turn_off_ssl_validation() # reload user agent to reflect that we have turned off ssl validation req_headers["user-agent"] = "; ".join(self._user_agents) @@ -3648,8 +3945,8 @@ def _make_call(self, verb, path, body, headers): raise LOG.debug( - "Request failed, attempt %d of %d. Retrying in %.2f seconds..." % - (attempt, max_rpc_attempts, rpc_attempt_interval) + "Request failed, attempt %d of %d. Retrying in %.2f seconds..." + % (attempt, max_rpc_attempts, rpc_attempt_interval) ) time.sleep(rpc_attempt_interval) @@ -3657,7 +3954,9 @@ def _http_request(self, verb, path, body, headers): """ Make the actual HTTP request. """ - url = urllib.parse.urlunparse((self.config.scheme, self.config.server, path, None, None, None)) + url = urllib.parse.urlunparse( + (self.config.scheme, self.config.server, path, None, None, None) + ) LOG.debug("Request is %s:%s" % (verb, url)) LOG.debug("Request headers are %s" % headers) LOG.debug("Request body is %s" % body) @@ -3666,10 +3965,7 @@ def _http_request(self, verb, path, body, headers): resp, content = conn.request(url, method=verb, body=body, headers=headers) # http response code is handled else where http_status = (resp.status, resp.reason) - resp_headers = dict( - (k.lower(), v) - for k, v in six.iteritems(resp) - ) + resp_headers = dict((k.lower(), v) for k, v in six.iteritems(resp)) resp_body = content LOG.debug("Response status is %s %s" % http_status) @@ -3704,10 +4000,7 @@ def _parse_http_status(self, status): headers = "HTTP error from server" if status[0] == 503: errmsg = "Flow Production Tracking is currently down for maintenance or too busy to reply. Please try again later." - raise ProtocolError(self.config.server, - error_code, - errmsg, - headers) + raise ProtocolError(self.config.server, error_code, errmsg, headers) return @@ -3739,6 +4032,7 @@ def _json_loads_ascii(self, body): """ See http://stackoverflow.com/questions/956867 """ + def _decode_list(lst): newlist = [] for i in lst: @@ -3760,6 +4054,7 @@ def _decode_dict(dct): v = _decode_list(v) newdict[k] = v return newdict + return json.loads(body, object_hook=_decode_dict) def _response_errors(self, sg_response): @@ -3780,21 +4075,28 @@ def _response_errors(self, sg_response): if isinstance(sg_response, dict) and sg_response.get("exception"): if sg_response.get("error_code") == ERR_AUTH: - raise AuthenticationFault(sg_response.get("message", "Unknown Authentication Error")) + raise AuthenticationFault( + sg_response.get("message", "Unknown Authentication Error") + ) elif sg_response.get("error_code") == ERR_2FA: raise MissingTwoFactorAuthenticationFault( sg_response.get("message", "Unknown 2FA Authentication Error") ) elif sg_response.get("error_code") == ERR_SSO: raise UserCredentialsNotAllowedForSSOAuthenticationFault( - sg_response.get("message", - "Authentication using username/password is not " - "allowed for an SSO-enabled Flow Production Tracking site") + sg_response.get( + "message", + "Authentication using username/password is not " + "allowed for an SSO-enabled Flow Production Tracking site", + ) ) elif sg_response.get("error_code") == ERR_OXYG: raise UserCredentialsNotAllowedForOxygenAuthenticationFault( - sg_response.get("message", "Authentication using username/password is not " - "allowed for an Autodesk Identity enabled Flow Production Tracking site") + sg_response.get( + "message", + "Authentication using username/password is not " + "allowed for an Autodesk Identity enabled Flow Production Tracking site", + ) ) else: # raise general Fault @@ -3817,10 +4119,7 @@ def _visit_data(self, data, visitor): return tuple(recursive(i, visitor) for i in data) if isinstance(data, dict): - return dict( - (k, recursive(v, visitor)) - for k, v in six.iteritems(data) - ) + return dict((k, recursive(v, visitor)) for k, v in six.iteritems(data)) return visitor(data) @@ -3833,10 +4132,12 @@ def _transform_outbound(self, data): """ if self.config.convert_datetimes_to_utc: + def _change_tz(value): if value.tzinfo is None: value = value.replace(tzinfo=SG_TIMEZONE.local) return value.astimezone(SG_TIMEZONE.utc) + else: _change_tz = None @@ -3859,7 +4160,7 @@ def _outbound_visitor(value): hour=value.hour, minute=value.minute, second=value.second, - microsecond=value.microsecond + microsecond=value.microsecond, ) if _change_tz: value = _change_tz(value) @@ -3881,8 +4182,10 @@ def _transform_inbound(self, data): # to the local time, otherwise it will fail to compare to datetimes # that do not have a time zone. if self.config.convert_datetimes_to_utc: + def _change_tz(x): return x.replace(tzinfo=SG_TIMEZONE.utc).astimezone(SG_TIMEZONE.local) + else: _change_tz = None @@ -3892,7 +4195,8 @@ def _inbound_visitor(value): try: # strptime was not on datetime in python2.4 value = datetime.datetime( - *time.strptime(value, "%Y-%m-%dT%H:%M:%SZ")[:6]) + *time.strptime(value, "%Y-%m-%dT%H:%M:%SZ")[:6] + ) except ValueError: return value if _change_tz: @@ -3914,14 +4218,26 @@ def _get_connection(self): return self._connection if self.config.proxy_server: - pi = ProxyInfo(socks.PROXY_TYPE_HTTP, self.config.proxy_server, - self.config.proxy_port, proxy_user=self.config.proxy_user, - proxy_pass=self.config.proxy_pass) - self._connection = Http(timeout=self.config.timeout_secs, ca_certs=self.__ca_certs, - proxy_info=pi, disable_ssl_certificate_validation=self.config.no_ssl_validation) + pi = ProxyInfo( + socks.PROXY_TYPE_HTTP, + self.config.proxy_server, + self.config.proxy_port, + proxy_user=self.config.proxy_user, + proxy_pass=self.config.proxy_pass, + ) + self._connection = Http( + timeout=self.config.timeout_secs, + ca_certs=self.__ca_certs, + proxy_info=pi, + disable_ssl_certificate_validation=self.config.no_ssl_validation, + ) else: - self._connection = Http(timeout=self.config.timeout_secs, ca_certs=self.__ca_certs, - proxy_info=None, disable_ssl_certificate_validation=self.config.no_ssl_validation) + self._connection = Http( + timeout=self.config.timeout_secs, + ca_certs=self.__ca_certs, + proxy_info=None, + disable_ssl_certificate_validation=self.config.no_ssl_validation, + ) return self._connection @@ -3940,6 +4256,7 @@ def _close_connection(self): self._connection.connections.clear() self._connection = None return + # ======================================================================== # Utility @@ -3961,7 +4278,9 @@ def _parse_records(self, records): return [] if not isinstance(records, (list, tuple)): - records = [records, ] + records = [ + records, + ] for rec in records: # skip results that aren't entity dictionaries @@ -3978,11 +4297,19 @@ def _parse_records(self, records): rec[k] = rec[k].replace("<", "<") # check for thumbnail for older version (<3.3.0) of shotgun - if k == "image" and self.server_caps.version and self.server_caps.version < (3, 3, 0): + if ( + k == "image" + and self.server_caps.version + and self.server_caps.version < (3, 3, 0) + ): rec["image"] = self._build_thumb_url(rec["type"], rec["id"]) continue - if isinstance(v, dict) and v.get("link_type") == "local" and self.client_caps.local_path_field in v: + if ( + isinstance(v, dict) + and v.get("link_type") == "local" + and self.client_caps.local_path_field in v + ): local_path = v[self.client_caps.local_path_field] v["local_path"] = local_path v["url"] = "file://%s" % (local_path or "",) @@ -4003,10 +4330,14 @@ def _build_thumb_url(self, entity_type, entity_id): # curl "https://foo.com/upload/get_thumbnail_url?entity_type=Version&entity_id=1" # 1 # /files/0000/0000/0012/232/shot_thumb.jpg.jpg - entity_info = {"e_type": urllib.parse.quote(entity_type), - "e_id": urllib.parse.quote(str(entity_id))} - url = ("/upload/get_thumbnail_url?" + - "entity_type=%(e_type)s&entity_id=%(e_id)s" % entity_info) + entity_info = { + "e_type": urllib.parse.quote(entity_type), + "e_id": urllib.parse.quote(str(entity_id)), + } + url = ( + "/upload/get_thumbnail_url?" + + "entity_type=%(e_type)s&entity_id=%(e_id)s" % entity_info + ) body = self._make_call("GET", url, None, None)[2] @@ -4018,15 +4349,23 @@ def _build_thumb_url(self, entity_type, entity_id): raise ShotgunError(thumb_url) if code == 1: - return urllib.parse.urlunparse((self.config.scheme, - self.config.server, - thumb_url.strip(), - None, None, None)) + return urllib.parse.urlunparse( + ( + self.config.scheme, + self.config.server, + thumb_url.strip(), + None, + None, + None, + ) + ) # Comments in prev version said we can get this sometimes. raise RuntimeError("Unknown code %s %s" % (code, thumb_url)) - def _dict_to_list(self, d, key_name="field_name", value_name="value", extra_data=None): + def _dict_to_list( + self, d, key_name="field_name", value_name="value", extra_data=None + ): """ Utility function to convert a dict into a list dicts using the key_name and value_name keys. @@ -4098,8 +4437,14 @@ def _multipart_upload_file_to_storage(self, path, upload_info): # encoded. data = BytesIO(data) bytes_read += data_size - part_url = self._get_upload_part_link(upload_info, filename, part_number) - etags.append(self._upload_data_to_storage(data, content_type, data_size, part_url)) + part_url = self._get_upload_part_link( + upload_info, filename, part_number + ) + etags.append( + self._upload_data_to_storage( + data, content_type, data_size, part_url + ) + ) part_number += 1 self._complete_multipart_upload(upload_info, filename, etags) @@ -4124,11 +4469,19 @@ def _get_upload_part_link(self, upload_info, filename, part_number): "filename": filename, "timestamp": upload_info["timestamp"], "upload_id": upload_info["upload_id"], - "part_number": part_number + "part_number": part_number, } - url = urllib.parse.urlunparse((self.config.scheme, self.config.server, - "/upload/api_get_upload_link_for_part", None, None, None)) + url = urllib.parse.urlunparse( + ( + self.config.scheme, + self.config.server, + "/upload/api_get_upload_link_for_part", + None, + None, + None, + ) + ) result = self._send_form(url, params) # Response is of the form: 1\n (for success) or 0\n (for failure). @@ -4172,9 +4525,15 @@ def _upload_data_to_storage(self, data, content_type, size, storage_url): attempt += 1 continue elif e.code in [500, 503]: - raise ShotgunError("Got a %s response when uploading to %s: %s" % (e.code, storage_url, e)) + raise ShotgunError( + "Got a %s response when uploading to %s: %s" + % (e.code, storage_url, e) + ) else: - raise ShotgunError("Unanticipated error occurred uploading to %s: %s" % (storage_url, e)) + raise ShotgunError( + "Unanticipated error occurred uploading to %s: %s" + % (storage_url, e) + ) except urllib.error.URLError as e: LOG.debug("Got a '%s' response. Waiting and retrying..." % e) time.sleep(float(attempt) * self.BACKOFF) @@ -4203,11 +4562,19 @@ def _complete_multipart_upload(self, upload_info, filename, etags): "filename": filename, "timestamp": upload_info["timestamp"], "upload_id": upload_info["upload_id"], - "etags": ",".join(etags) + "etags": ",".join(etags), } - url = urllib.parse.urlunparse((self.config.scheme, self.config.server, - "/upload/api_complete_multipart_upload", None, None, None)) + url = urllib.parse.urlunparse( + ( + self.config.scheme, + self.config.server, + "/upload/api_complete_multipart_upload", + None, + None, + None, + ) + ) result = self._send_form(url, params) # Response is of the form: 1\n or 0\n to indicate success or failure of the call. @@ -4283,8 +4650,11 @@ def _send_form(self, url, params): continue except urllib.error.HTTPError as e: if e.code == 500: - raise ShotgunError("Server encountered an internal error. " - "\n%s\n(%s)\n%s\n\n" % (url, self._sanitize_auth_params(params), e)) + raise ShotgunError( + "Server encountered an internal error. " + "\n%s\n(%s)\n%s\n\n" + % (url, self._sanitize_auth_params(params), e) + ) else: raise ShotgunError("Unanticipated error occurred %s" % (e)) @@ -4294,7 +4664,7 @@ def _send_form(self, url, params): class CACertsHTTPSConnection(http_client.HTTPConnection): - """" + """ " This class allows to create an HTTPS connection that uses the custom certificates passed in. """ @@ -4324,9 +4694,7 @@ def connect(self): self.sock = context.wrap_socket(self.sock) else: self.sock = ssl.wrap_socket( - self.sock, - ca_certs=self.__ca_certs, - cert_reqs=ssl.CERT_REQUIRED + self.sock, ca_certs=self.__ca_certs, cert_reqs=ssl.CERT_REQUIRED ) @@ -4352,6 +4720,7 @@ class FormPostHandler(urllib.request.BaseHandler): """ Handler for multipart form data """ + handler_order = urllib.request.HTTPHandler.handler_order - 10 # needs to run first def http_request(self, request): @@ -4370,7 +4739,9 @@ def http_request(self, request): else: params.append((key, value)) if not files: - data = sgutils.ensure_binary(urllib.parse.urlencode(params, True)) # sequencing on + data = sgutils.ensure_binary( + urllib.parse.urlencode(params, True) + ) # sequencing on else: boundary, data = self.encode(params, files) content_type = "multipart/form-data; boundary=%s" % boundary @@ -4392,7 +4763,7 @@ def encode(self, params, files, boundary=None, buffer=None): boundary = uuid.uuid4() if buffer is None: buffer = BytesIO() - for (key, value) in params: + for key, value in params: if not isinstance(value, str): # If value is not a string (e.g. int) cast to text value = str(value) @@ -4400,9 +4771,11 @@ def encode(self, params, files, boundary=None, buffer=None): key = sgutils.ensure_text(key) buffer.write(sgutils.ensure_binary("--%s\r\n" % boundary)) - buffer.write(sgutils.ensure_binary("Content-Disposition: form-data; name=\"%s\"" % key)) + buffer.write( + sgutils.ensure_binary('Content-Disposition: form-data; name="%s"' % key) + ) buffer.write(sgutils.ensure_binary("\r\n\r\n%s\r\n" % value)) - for (key, fd) in files: + for key, fd in files: # On Windows, it's possible that we were forced to open a file # with non-ascii characters as unicode. In that case, we need to # encode it as a utf-8 string to remove unicode from the equation. @@ -4416,7 +4789,7 @@ def encode(self, params, files, boundary=None, buffer=None): content_type = content_type or "application/octet-stream" file_size = os.fstat(fd.fileno())[stat.ST_SIZE] buffer.write(sgutils.ensure_binary("--%s\r\n" % boundary)) - c_dis = "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"%s" + c_dis = 'Content-Disposition: form-data; name="%s"; filename="%s"%s' content_disposition = c_dis % (key, filename, "\r\n") buffer.write(sgutils.ensure_binary(content_disposition)) buffer.write(sgutils.ensure_binary("Content-Type: %s\r\n" % content_type)) @@ -4438,10 +4811,7 @@ def _translate_filters(filters, filter_operator): """ Translate filters params into data structure expected by rpc call. """ - wrapped_filters = { - "filter_operator": filter_operator or "all", - "filters": filters - } + wrapped_filters = {"filter_operator": filter_operator or "all", "filters": filters} return _translate_filters_dict(wrapped_filters) @@ -4458,8 +4828,9 @@ def _translate_filters_dict(sg_filter): raise ShotgunError("Invalid filter_operator %s" % filter_operator) if not isinstance(sg_filter["filters"], (list, tuple)): - raise ShotgunError("Invalid filters, expected a list or a tuple, got %s" - % sg_filter["filters"]) + raise ShotgunError( + "Invalid filters, expected a list or a tuple, got %s" % sg_filter["filters"] + ) new_filters["conditions"] = _translate_filters_list(sg_filter["filters"]) @@ -4475,17 +4846,15 @@ def _translate_filters_list(filters): elif isinstance(sg_filter, dict): conditions.append(_translate_filters_dict(sg_filter)) else: - raise ShotgunError("Invalid filters, expected a list, tuple or dict, got %s" - % sg_filter) + raise ShotgunError( + "Invalid filters, expected a list, tuple or dict, got %s" % sg_filter + ) return conditions def _translate_filters_simple(sg_filter): - condition = { - "path": sg_filter[0], - "relation": sg_filter[1] - } + condition = {"path": sg_filter[0], "relation": sg_filter[1]} values = sg_filter[2:] if len(values) == 1 and isinstance(values[0], (list, tuple)): @@ -4523,7 +4892,7 @@ def _get_type_and_id_from_value(value): if isinstance(value, dict): return {"type": value["type"], "id": value["id"]} elif isinstance(value, list): - return [{"type": v["type"], "id": v["id"]} for v in value] + return [{"type": v["type"], "id": v["id"]} for v in value] except (KeyError, TypeError): LOG.debug(f"Could not optimize entity value {value}") diff --git a/tests/base.py b/tests/base.py index 4eaafb867..2820d495d 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,4 +1,5 @@ """Base class for Flow Production Tracking API tests.""" + import contextlib import os import random @@ -33,9 +34,9 @@ def skip(f): class TestBase(unittest.TestCase): - '''Base class for tests. + """Base class for tests. - Sets up mocking and database test data.''' + Sets up mocking and database test data.""" human_user = None project = None @@ -74,7 +75,7 @@ def setUpClass(cls): script_name=cls.config.script_name, api_key=cls.config.api_key ) - def setUp(self, auth_mode='ApiUser'): + def setUp(self, auth_mode="ApiUser"): # When running the tests from a pull request from a client, the Shotgun # site URL won't be set, so do not attempt to run the test. if not self.config.server_url: @@ -88,30 +89,38 @@ def setUp(self, auth_mode='ApiUser'): self.http_proxy = self.config.http_proxy self.session_uuid = self.config.session_uuid - if auth_mode == 'ApiUser': - self.sg = api.Shotgun(self.config.server_url, - self.config.script_name, - self.config.api_key, - http_proxy=self.config.http_proxy, - connect=self.connect) - elif auth_mode == 'HumanUser': - self.sg = api.Shotgun(self.config.server_url, - login=self.human_login, - password=self.human_password, - http_proxy=self.config.http_proxy, - connect=self.connect) - elif auth_mode == 'SessionToken': + if auth_mode == "ApiUser": + self.sg = api.Shotgun( + self.config.server_url, + self.config.script_name, + self.config.api_key, + http_proxy=self.config.http_proxy, + connect=self.connect, + ) + elif auth_mode == "HumanUser": + self.sg = api.Shotgun( + self.config.server_url, + login=self.human_login, + password=self.human_password, + http_proxy=self.config.http_proxy, + connect=self.connect, + ) + elif auth_mode == "SessionToken": # first make an instance based on script key/name so # we can generate a session token - sg = api.Shotgun(self.config.server_url, - http_proxy=self.config.http_proxy, - **self.auth_args) + sg = api.Shotgun( + self.config.server_url, + http_proxy=self.config.http_proxy, + **self.auth_args, + ) self.session_token = sg.get_session_token() # now log in using session token - self.sg = api.Shotgun(self.config.server_url, - session_token=self.session_token, - http_proxy=self.config.http_proxy, - connect=self.connect) + self.sg = api.Shotgun( + self.config.server_url, + session_token=self.session_token, + http_proxy=self.config.http_proxy, + connect=self.connect, + ) else: raise ValueError("Unknown value for auth_mode: %s" % auth_mode) @@ -123,7 +132,7 @@ def tearDown(self): class MockTestBase(TestBase): - '''Test base for tests mocking server interactions.''' + """Test base for tests mocking server interactions.""" def setUp(self): super(MockTestBase, self).setUp() @@ -132,23 +141,25 @@ def setUp(self): self._setup_mock_data() def _setup_mock(self, s3_status_code_error=503): - """Setup mocking on the ShotgunClient to stop it calling a live server - """ + """Setup mocking on the ShotgunClient to stop it calling a live server""" # Replace the function used to make the final call to the server # eaiser than mocking the http connection + response - self.sg._http_request = mock.Mock(spec=api.Shotgun._http_request, - return_value=((200, "OK"), {}, None)) + self.sg._http_request = mock.Mock( + spec=api.Shotgun._http_request, return_value=((200, "OK"), {}, None) + ) # Replace the function used to make the final call to the S3 server, and simulate # the exception HTTPError raised with 503 status errors - self.sg._make_upload_request = mock.Mock(spec=api.Shotgun._make_upload_request, - side_effect = urllib.error.HTTPError( - "url", - s3_status_code_error, - "The server is currently down or to busy to reply." - "Please try again later.", - {}, - None - )) + self.sg._make_upload_request = mock.Mock( + spec=api.Shotgun._make_upload_request, + side_effect=urllib.error.HTTPError( + "url", + s3_status_code_error, + "The server is currently down or to busy to reply." + "Please try again later.", + {}, + None, + ), + ) # also replace the function that is called to get the http connection # to avoid calling the server. OK to return a mock as we will not use # it @@ -160,8 +171,9 @@ def _setup_mock(self, s3_status_code_error=503): self.sg._get_connection = mock.Mock(return_value=self.mock_conn) # create the server caps directly to say we have the correct version - self.sg._server_caps = ServerCapabilities(self.sg.config.server, - {"version": [2, 4, 0]}) + self.sg._server_caps = ServerCapabilities( + self.sg.config.server, {"version": [2, 4, 0]} + ) # prevent waiting for backoff self.sg.BACKOFF = 0 @@ -177,24 +189,22 @@ def _mock_http(self, data, headers=None, status=None): if not isinstance(data, str): if six.PY2: - data = json.dumps( - data, - ensure_ascii=False, - encoding="utf-8" - ) + data = json.dumps(data, ensure_ascii=False, encoding="utf-8") else: data = json.dumps( data, ensure_ascii=False, ) - resp_headers = {'cache-control': 'no-cache', - 'connection': 'close', - 'content-length': (data and str(len(data))) or 0, - 'content-type': 'application/json; charset=utf-8', - 'date': 'Wed, 13 Apr 2011 04:18:58 GMT', - 'server': 'Apache/2.2.3 (CentOS)', - 'status': '200 OK'} + resp_headers = { + "cache-control": "no-cache", + "connection": "close", + "content-length": (data and str(len(data))) or 0, + "content-type": "application/json; charset=utf-8", + "date": "Wed, 13 Apr 2011 04:18:58 GMT", + "server": "Apache/2.2.3 (CentOS)", + "status": "200 OK", + } if headers: resp_headers.update(headers) @@ -220,42 +230,40 @@ def _assert_http_method(self, method, params, check_auth=True): self.assertEqual(self.api_key, auth["script_key"]) if params: - rpc_args = arg_params[len(arg_params)-1] + rpc_args = arg_params[len(arg_params) - 1] self.assertEqual(params, rpc_args) def _setup_mock_data(self): - self.human_user = {'id': 1, - 'login': self.config.human_login, - 'type': 'HumanUser'} - self.project = {'id': 2, - 'name': self.config.project_name, - 'type': 'Project'} - self.shot = {'id': 3, - 'code': self.config.shot_code, - 'type': 'Shot'} - self.asset = {'id': 4, - 'code': self.config.asset_code, - 'type': 'Asset'} - self.version = {'id': 5, - 'code': self.config.version_code, - 'type': 'Version'} - self.playlist = {'id': 7, - 'code': self.config.playlist_code, - 'type': 'Playlist'} + self.human_user = { + "id": 1, + "login": self.config.human_login, + "type": "HumanUser", + } + self.project = {"id": 2, "name": self.config.project_name, "type": "Project"} + self.shot = {"id": 3, "code": self.config.shot_code, "type": "Shot"} + self.asset = {"id": 4, "code": self.config.asset_code, "type": "Asset"} + self.version = {"id": 5, "code": self.config.version_code, "type": "Version"} + self.playlist = {"id": 7, "code": self.config.playlist_code, "type": "Playlist"} class LiveTestBase(TestBase): - '''Test base for tests relying on connection to server.''' + """Test base for tests relying on connection to server.""" def setUp(self, auth_mode=None): if not auth_mode: - auth_mode = 'HumanUser' if self.config.jenkins else 'ApiUser' + auth_mode = "HumanUser" if self.config.jenkins else "ApiUser" super(LiveTestBase, self).setUp(auth_mode) - if self.sg.server_caps.version and \ - self.sg.server_caps.version >= (3, 3, 0) and \ - (self.sg.server_caps.host.startswith('0.0.0.0') or - self.sg.server_caps.host.startswith('127.0.0.1')): - self.server_address = re.sub('^0.0.0.0|127.0.0.1', 'localhost', self.sg.server_caps.host) + if ( + self.sg.server_caps.version + and self.sg.server_caps.version >= (3, 3, 0) + and ( + self.sg.server_caps.host.startswith("0.0.0.0") + or self.sg.server_caps.host.startswith("127.0.0.1") + ) + ): + self.server_address = re.sub( + "^0.0.0.0|127.0.0.1", "localhost", self.sg.server_caps.host + ) else: self.server_address = self.sg.server_caps.host @@ -279,73 +287,75 @@ def setUpClass(cls): cls.config.server_url, **cls.auth_args, ) - cls.sg_version = tuple(sg.info()['version'][:3]) + cls.sg_version = tuple(sg.info()["version"][:3]) cls._setup_db(cls.config, sg) @classmethod def _setup_db(cls, config, sg): - data = {'name': cls.config.project_name} - cls.project = _find_or_create_entity(sg, 'Project', data) - - data = {'name': cls.config.human_name, - 'login': cls.config.human_login, - 'password_proxy': cls.config.human_password} + data = {"name": cls.config.project_name} + cls.project = _find_or_create_entity(sg, "Project", data) + + data = { + "name": cls.config.human_name, + "login": cls.config.human_login, + "password_proxy": cls.config.human_password, + } if cls.sg_version >= (3, 0, 0): - data['locked_until'] = None - - cls.human_user = _find_or_create_entity(sg, 'HumanUser', data) - - data = {'code': cls.config.asset_code, - 'project': cls.project} - keys = ['code'] - cls.asset = _find_or_create_entity(sg, 'Asset', data, keys) - - data = {'project': cls.project, - 'code': cls.config.version_code, - 'entity': cls.asset, - 'user': cls.human_user, - 'sg_frames_aspect_ratio': 13.3, - 'frame_count': 33} - keys = ['code', 'project'] - cls.version = _find_or_create_entity(sg, 'Version', data, keys) - - keys = ['code', 'project'] - data = {'code': cls.config.shot_code, - 'project': cls.project} - cls.shot = _find_or_create_entity(sg, 'Shot', data, keys) - - keys = ['project', 'user'] - data = {'project': cls.project, - 'user': cls.human_user, - 'content': 'anything'} - cls.note = _find_or_create_entity(sg, 'Note', data, keys) - - keys = ['code', 'project'] - data = {'project': cls.project, - 'code': cls.config.playlist_code} - cls.playlist = _find_or_create_entity(sg, 'Playlist', data, keys) - - keys = ['code', 'entity_type'] - data = {'code': 'wrapper test step', - 'entity_type': 'Shot'} - cls.step = _find_or_create_entity(sg, 'Step', data, keys) - - keys = ['project', 'entity', 'content'] - data = {'project': cls.project, - 'entity': cls.asset, - 'content': cls.config.task_content, - 'color': 'Black', - 'due_date': '1968-10-13', - 'task_assignees': [cls.human_user], - 'sg_status_list': 'ip'} - cls.task = _find_or_create_entity(sg, 'Task', data, keys) - - keys = ['code'] - data = {'code': 'api wrapper test storage', - 'mac_path': 'nowhere', - 'windows_path': 'nowhere', - 'linux_path': 'nowhere'} - cls.local_storage = _find_or_create_entity(sg, 'LocalStorage', data, keys) + data["locked_until"] = None + + cls.human_user = _find_or_create_entity(sg, "HumanUser", data) + + data = {"code": cls.config.asset_code, "project": cls.project} + keys = ["code"] + cls.asset = _find_or_create_entity(sg, "Asset", data, keys) + + data = { + "project": cls.project, + "code": cls.config.version_code, + "entity": cls.asset, + "user": cls.human_user, + "sg_frames_aspect_ratio": 13.3, + "frame_count": 33, + } + keys = ["code", "project"] + cls.version = _find_or_create_entity(sg, "Version", data, keys) + + keys = ["code", "project"] + data = {"code": cls.config.shot_code, "project": cls.project} + cls.shot = _find_or_create_entity(sg, "Shot", data, keys) + + keys = ["project", "user"] + data = {"project": cls.project, "user": cls.human_user, "content": "anything"} + cls.note = _find_or_create_entity(sg, "Note", data, keys) + + keys = ["code", "project"] + data = {"project": cls.project, "code": cls.config.playlist_code} + cls.playlist = _find_or_create_entity(sg, "Playlist", data, keys) + + keys = ["code", "entity_type"] + data = {"code": "wrapper test step", "entity_type": "Shot"} + cls.step = _find_or_create_entity(sg, "Step", data, keys) + + keys = ["project", "entity", "content"] + data = { + "project": cls.project, + "entity": cls.asset, + "content": cls.config.task_content, + "color": "Black", + "due_date": "1968-10-13", + "task_assignees": [cls.human_user], + "sg_status_list": "ip", + } + cls.task = _find_or_create_entity(sg, "Task", data, keys) + + keys = ["code"] + data = { + "code": "api wrapper test storage", + "mac_path": "nowhere", + "windows_path": "nowhere", + "linux_path": "nowhere", + } + cls.local_storage = _find_or_create_entity(sg, "LocalStorage", data, keys) @contextlib.contextmanager def gen_entity(self, entity_type, **kwargs): @@ -360,7 +370,7 @@ def gen_entity(self, entity_type, **kwargs): if "password_proxy" not in kwargs: kwargs["password_proxy"] = self.config.human_password - item_rnd = random.randrange(100,999) + item_rnd = random.randrange(100, 999) for k in kwargs: if isinstance(kwargs[k], str): kwargs[k] = kwargs[k].format(rnd=item_rnd) @@ -372,13 +382,20 @@ def gen_entity(self, entity_type, **kwargs): rv = self.sg.delete(entity_type, entity["id"]) assert rv == True - def find_one_await_thumbnail(self, entity_type, filters, fields=["image"], thumbnail_field_name="image", **kwargs): + def find_one_await_thumbnail( + self, + entity_type, + filters, + fields=["image"], + thumbnail_field_name="image", + **kwargs + ): attempts = 0 while attempts < THUMBNAIL_MAX_ATTEMPTS: result = self.sg.find_one(entity_type, filters, fields=fields, **kwargs) if TRANSIENT_IMAGE_PATH in result.get(thumbnail_field_name, ""): return result - + time.sleep(THUMBNAIL_RETRY_INTERVAL) attempts += 1 else: @@ -387,43 +404,55 @@ def find_one_await_thumbnail(self, entity_type, filters, fields=["image"], thumb class HumanUserAuthLiveTestBase(LiveTestBase): - ''' + """ Test base for relying on a Shotgun connection authenticate through the configured login/password pair. - ''' + """ def setUp(self): - super(HumanUserAuthLiveTestBase, self).setUp('HumanUser') + super(HumanUserAuthLiveTestBase, self).setUp("HumanUser") class SessionTokenAuthLiveTestBase(LiveTestBase): - ''' + """ Test base for relying on a Shotgun connection authenticate through the configured session_token parameter. - ''' + """ def setUp(self): - super(SessionTokenAuthLiveTestBase, self).setUp('SessionToken') + super(SessionTokenAuthLiveTestBase, self).setUp("SessionToken") class SgTestConfig(object): - '''Reads test config and holds values''' + """Reads test config and holds values""" def __init__(self): for key in self.config_keys(): # Look for any environment variables that match our test # configuration naming of "SG_{KEY}". Default is None. - value = os.environ.get('SG_%s' % (str(key).upper())) - if key in ['mock']: - value = (value is None) or (str(value).lower() in ['true', '1']) + value = os.environ.get("SG_%s" % (str(key).upper())) + if key in ["mock"]: + value = (value is None) or (str(value).lower() in ["true", "1"]) setattr(self, key, value) def config_keys(self): return [ - 'api_key', 'asset_code', 'http_proxy', 'human_login', 'human_name', - 'human_password', 'mock', 'project_name', 'script_name', - 'server_url', 'session_uuid', 'shot_code', 'task_content', - 'version_code', 'playlist_code', 'jenkins' + "api_key", + "asset_code", + "http_proxy", + "human_login", + "human_name", + "human_password", + "mock", + "project_name", + "script_name", + "server_url", + "session_uuid", + "shot_code", + "task_content", + "version_code", + "playlist_code", + "jenkins", ] def read_config(self, config_path): @@ -439,7 +468,7 @@ def read_config(self, config_path): def _find_or_create_entity(sg, entity_type, data, identifyiers=None): - '''Finds or creates entities. + """Finds or creates entities. @params: sg - shogun_json.Shotgun instance entity_type - entity type @@ -447,11 +476,11 @@ def _find_or_create_entity(sg, entity_type, data, identifyiers=None): identifyiers -list of subset of keys from data which should be used to uniquely identity the entity @returns dicitonary of the entity values - ''' - identifyiers = identifyiers or ['name'] + """ + identifyiers = identifyiers or ["name"] fields = list(data.keys()) - filters = [[key, 'is', data[key]] for key in identifyiers] + filters = [[key, "is", data[key]] for key in identifyiers] entity = sg.find_one(entity_type, filters, fields=fields) entity = entity or sg.create(entity_type, data, return_fields=fields) - assert(entity) + assert entity return entity diff --git a/tests/ci_requirements.txt b/tests/ci_requirements.txt index 92189202a..5c2074965 100644 --- a/tests/ci_requirements.txt +++ b/tests/ci_requirements.txt @@ -8,14 +8,14 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. +coverage coveralls==1.1 -nose==1.3.7 -nose-exclude==0.5.0 # Don't restrict flake8 version, since we install this in CI against Python 2.6, # where flake8 has discontinued support for newer releases. On Python 2.7 and # Python 3.7, linting has been performed with flake8 3.7.8 flake8 +nose==1.3.7 +nose-exclude==0.5.0 pytest pytest-azurepipelines -coverage pytest-coverage diff --git a/tests/mock.py b/tests/mock.py index 456c02594..736571c64 100644 --- a/tests/mock.py +++ b/tests/mock.py @@ -14,16 +14,16 @@ __all__ = ( - 'Mock', - 'MagicMock', - 'mocksignature', - 'patch', - 'patch_object', - 'sentinel', - 'DEFAULT' + "Mock", + "MagicMock", + "mocksignature", + "patch", + "patch_object", + "sentinel", + "DEFAULT", ) -__version__ = '0.7.0' +__version__ = "0.7.0" __unittest = True @@ -48,8 +48,10 @@ def inner(f): f.__doc__ = original.__doc__ f.__module__ = original.__module__ return f + return inner + try: unicode except NameError: @@ -65,18 +67,19 @@ def inner(f): inPy3k = sys.version_info[0] == 3 if inPy3k: - self = '__self__' + self = "__self__" else: - self = 'im_self' + self = "im_self" # getsignature and mocksignature heavily "inspired" by # the decorator module: http://pypi.python.org/pypi/decorator/ # by Michele Simionato + def _getsignature(func, skipfirst): if inspect is None: - raise ImportError('inspect module not available') + raise ImportError("inspect module not available") if inspect.isclass(func): func = func.__init__ @@ -92,15 +95,16 @@ def _getsignature(func, skipfirst): regargs = regargs[1:] _msg = "_mock_ is a reserved argument name, can't mock signatures using _mock_" - assert '_mock_' not in regargs, _msg + assert "_mock_" not in regargs, _msg if varargs is not None: - assert '_mock_' not in varargs, _msg + assert "_mock_" not in varargs, _msg if varkwargs is not None: - assert '_mock_' not in varkwargs, _msg + assert "_mock_" not in varkwargs, _msg if skipfirst: regargs = regargs[1:] - signature = inspect.formatargspec(regargs, varargs, varkwargs, defaults, - formatvalue=lambda value: "") + signature = inspect.formatargspec( + regargs, varargs, varkwargs, defaults, formatvalue=lambda value: "" + ) return signature[1:-1], func @@ -138,9 +142,7 @@ def mocksignature(func, mock=None, skipfirst=False): if mock is None: mock = Mock() signature, func = _getsignature(func, skipfirst) - src = "lambda %(signature)s: _mock_(%(signature)s)" % { - 'signature': signature - } + src = "lambda %(signature)s: _mock_(%(signature)s)" % {"signature": signature} funcopy = eval(src, dict(_mock_=mock)) _copy_func_details(func, funcopy) @@ -149,11 +151,12 @@ def mocksignature(func, mock=None, skipfirst=False): def _is_magic(name): - return '__%s__' % name[2:-2] == name + return "__%s__" % name[2:-2] == name class SentinelObject(object): "A unique, named, sentinel object." + def __init__(self, name): self.name = name @@ -163,11 +166,12 @@ def __repr__(self): class Sentinel(object): """Access attributes to return a named object, usable as a sentinel.""" + def __init__(self): self._sentinels = {} def __getattr__(self, name): - if name == '__bases__': + if name == "__bases__": # Without this help(mock) raises an exception raise AttributeError return self._sentinels.setdefault(name, SentinelObject(name)) @@ -180,6 +184,8 @@ def __getattr__(self, name): class OldStyleClass: pass + + ClassType = type(OldStyleClass) @@ -241,16 +247,24 @@ class or instance) that acts as the specification for the mock object. If mock. This can be useful for debugging. The name is propagated to child mocks. """ + def __new__(cls, *args, **kw): # every instance has its own class # so we can create magic methods on the # class without stomping on other mocks - new = type(cls.__name__, (cls,), {'__doc__': cls.__doc__}) + new = type(cls.__name__, (cls,), {"__doc__": cls.__doc__}) return object.__new__(new) - - def __init__(self, spec=None, side_effect=None, return_value=DEFAULT, - wraps=None, name=None, spec_set=None, parent=None): + def __init__( + self, + spec=None, + side_effect=None, + return_value=DEFAULT, + wraps=None, + name=None, + spec_set=None, + parent=None, + ): self._parent = parent self._name = name _spec_class = None @@ -275,14 +289,12 @@ def __init__(self, spec=None, side_effect=None, return_value=DEFAULT, self.reset_mock() - @property def __class__(self): if self._spec_class is None: return type(self) return self._spec_class - def reset_mock(self): "Restore the mock object to its initial state." self.called = False @@ -296,7 +308,6 @@ def reset_mock(self): if not self._return_value is self: self._return_value.reset_mock() - def __get_return_value(self): if self._return_value is DEFAULT: self._return_value = self._get_child_mock() @@ -306,9 +317,7 @@ def __set_return_value(self, value): self._return_value = value __return_value_doc = "The value to be returned when the mock is called." - return_value = property(__get_return_value, __set_return_value, - __return_value_doc) - + return_value = property(__get_return_value, __set_return_value, __return_value_doc) def __call__(self, *args, **kwargs): self.called = True @@ -322,14 +331,16 @@ def __call__(self, *args, **kwargs): parent.method_calls.append(callargs((name, args, kwargs))) if parent._parent is None: break - name = parent._name + '.' + name + name = parent._name + "." + name parent = parent._parent ret_val = DEFAULT if self.side_effect is not None: - if (isinstance(self.side_effect, BaseException) or - isinstance(self.side_effect, class_types) and - issubclass(self.side_effect, BaseException)): + if ( + isinstance(self.side_effect, BaseException) + or isinstance(self.side_effect, class_types) + and issubclass(self.side_effect, BaseException) + ): raise self.side_effect ret_val = self.side_effect(*args, **kwargs) @@ -342,9 +353,8 @@ def __call__(self, *args, **kwargs): ret_val = self.return_value return ret_val - def __getattr__(self, name): - if name == '_methods': + if name == "_methods": raise AttributeError(name) elif self._methods is not None: if name not in self._methods or name in _all_magics: @@ -356,49 +366,57 @@ def __getattr__(self, name): wraps = None if self._wraps is not None: wraps = getattr(self._wraps, name) - self._children[name] = self._get_child_mock(parent=self, name=name, wraps=wraps) + self._children[name] = self._get_child_mock( + parent=self, name=name, wraps=wraps + ) return self._children[name] - def __repr__(self): if self._name is None and self._spec_class is None: return object.__repr__(self) - name_string = '' - spec_string = '' + name_string = "" + spec_string = "" if self._name is not None: + def get_name(name): if name is None: - return 'mock' + return "mock" return name + parent = self._parent name = self._name while parent is not None: - name = get_name(parent._name) + '.' + name + name = get_name(parent._name) + "." + name parent = parent._parent - name_string = ' name=%r' % name + name_string = " name=%r" % name if self._spec_class is not None: - spec_string = ' spec=%r' + spec_string = " spec=%r" if self._spec_set: - spec_string = ' spec_set=%r' + spec_string = " spec_set=%r" spec_string = spec_string % self._spec_class.__name__ - return "<%s%s%s id='%s'>" % (type(self).__name__, - name_string, - spec_string, - id(self)) - + return "<%s%s%s id='%s'>" % ( + type(self).__name__, + name_string, + spec_string, + id(self), + ) def __setattr__(self, name, value): - if not 'method_calls' in self.__dict__: + if not "method_calls" in self.__dict__: # allow all attribute setting until initialisation is complete return object.__setattr__(self, name, value) - if (self._spec_set and self._methods is not None and name not in - self._methods and name not in self.__dict__ and - name != 'return_value'): + if ( + self._spec_set + and self._methods is not None + and name not in self._methods + and name not in self.__dict__ + and name != "return_value" + ): raise AttributeError("Mock object has no attribute '%s'" % name) if name in _unsupported_magics: - msg = 'Attempting to set unsupported magic method %r.' % name + msg = "Attempting to set unsupported magic method %r." % name raise AttributeError(msg) elif name in _all_magics: if self._methods is not None and name not in self._methods: @@ -413,13 +431,11 @@ def __setattr__(self, name, value): setattr(type(self), name, value) return object.__setattr__(self, name, value) - def __delattr__(self, name): if name in _all_magics and name in type(self).__dict__: delattr(type(self), name) return object.__delattr__(self, name) - def assert_called_with(self, *args, **kwargs): """ assert that the mock was called with the specified arguments. @@ -428,31 +444,27 @@ def assert_called_with(self, *args, **kwargs): different to the last call to the mock. """ if self.call_args is None: - raise AssertionError('Expected: %s\nNot called' % ((args, kwargs),)) + raise AssertionError("Expected: %s\nNot called" % ((args, kwargs),)) if not self.call_args == (args, kwargs): raise AssertionError( - 'Expected: %s\nCalled with: %s' % ((args, kwargs), self.call_args) + "Expected: %s\nCalled with: %s" % ((args, kwargs), self.call_args) ) - def assert_called_once_with(self, *args, **kwargs): """ assert that the mock was called exactly once and with the specified arguments. """ if not self.call_count == 1: - msg = ("Expected to be called once. Called %s times." % - self.call_count) + msg = "Expected to be called once. Called %s times." % self.call_count raise AssertionError(msg) return self.assert_called_with(*args, **kwargs) - def _get_child_mock(self, **kw): klass = type(self).__mro__[1] return klass(**kw) - class callargs(tuple): """ A tuple for holding the results of a call to a mock, either in the form @@ -465,6 +477,7 @@ class callargs(tuple): callargs('name', (1,), {}) == ('name', (1,)) callargs((), {'a': 'b'}) == ({'a': 'b'},) """ + def __eq__(self, other): if len(self) == 3: if other[0] != self[0]: @@ -499,7 +512,7 @@ def _dot_lookup(thing, comp, import_path): def _importer(target): - components = target.split('.') + components = target.split(".") import_path = components.pop(0) thing = __import__(import_path) @@ -510,8 +523,7 @@ def _importer(target): class _patch(object): - def __init__(self, target, attribute, new, spec, create, - mocksignature, spec_set): + def __init__(self, target, attribute, new, spec, create, mocksignature, spec_set): self.target = target self.attribute = attribute self.new = new @@ -521,11 +533,16 @@ def __init__(self, target, attribute, new, spec, create, self.mocksignature = mocksignature self.spec_set = spec_set - def copy(self): - return _patch(self.target, self.attribute, self.new, self.spec, - self.create, self.mocksignature, self.spec_set) - + return _patch( + self.target, + self.attribute, + self.new, + self.spec, + self.create, + self.mocksignature, + self.spec_set, + ) def __call__(self, func): if isinstance(func, class_types): @@ -533,7 +550,6 @@ def __call__(self, func): else: return self.decorate_callable(func) - def decorate_class(self, klass): for attr in dir(klass): attr_value = getattr(klass, attr) @@ -541,9 +557,8 @@ def decorate_class(self, klass): setattr(klass, attr, self.copy()(attr_value)) return klass - def decorate_callable(self, func): - if hasattr(func, 'patchings'): + if hasattr(func, "patchings"): func.patchings.append(self) return func @@ -559,17 +574,17 @@ def patched(*args, **keywargs): try: return func(*args, **keywargs) finally: - for patching in reversed(getattr(patched, 'patchings', [])): + for patching in reversed(getattr(patched, "patchings", [])): patching.__exit__() patched.patchings = [self] - if hasattr(func, 'func_code'): + if hasattr(func, "func_code"): # not in Python 3 - patched.compat_co_firstlineno = getattr(func, "compat_co_firstlineno", - func.func_code.co_firstlineno) + patched.compat_co_firstlineno = getattr( + func, "compat_co_firstlineno", func.func_code.co_firstlineno + ) return patched - def get_original(self): target = self.target name = self.attribute @@ -588,7 +603,6 @@ def get_original(self): raise AttributeError("%s does not have the attribute %r" % (target, name)) return original, local - def __enter__(self): """Perform the patch.""" new, spec, spec_set = self.new, self.spec, self.spec_set @@ -617,7 +631,6 @@ def __enter__(self): setattr(self.target, self.attribute, new_attr) return new - def __exit__(self, *_): """Undo the patch.""" if self.is_local and self.temp_original is not DEFAULT: @@ -635,8 +648,15 @@ def __exit__(self, *_): stop = __exit__ -def _patch_object(target, attribute, new=DEFAULT, spec=None, create=False, - mocksignature=False, spec_set=None): +def _patch_object( + target, + attribute, + new=DEFAULT, + spec=None, + create=False, + mocksignature=False, + spec_set=None, +): """ patch.object(target, attribute, new=DEFAULT, spec=None, create=False, mocksignature=False, spec_set=None) @@ -647,18 +667,18 @@ def _patch_object(target, attribute, new=DEFAULT, spec=None, create=False, Arguments new, spec, create, mocksignature and spec_set have the same meaning as for patch. """ - return _patch(target, attribute, new, spec, create, mocksignature, - spec_set) + return _patch(target, attribute, new, spec, create, mocksignature, spec_set) def patch_object(*args, **kwargs): "A deprecated form of patch.object(...)" - warnings.warn(('Please use patch.object instead.'), DeprecationWarning, 2) + warnings.warn(("Please use patch.object instead."), DeprecationWarning, 2) return _patch_object(*args, **kwargs) -def patch(target, new=DEFAULT, spec=None, create=False, - mocksignature=False, spec_set=None): +def patch( + target, new=DEFAULT, spec=None, create=False, mocksignature=False, spec_set=None +): """ ``patch`` acts as a function decorator, class decorator or a context manager. Inside the body of the function or with statement, the ``target`` @@ -707,10 +727,9 @@ def patch(target, new=DEFAULT, spec=None, create=False, use-cases. """ try: - target, attribute = target.rsplit('.', 1) + target, attribute = target.rsplit(".", 1) except (TypeError, ValueError): - raise TypeError("Need a valid target to patch. You supplied: %r" % - (target,)) + raise TypeError("Need a valid target to patch. You supplied: %r" % (target,)) target = _importer(target) return _patch(target, attribute, new, spec, create, mocksignature, spec_set) @@ -743,10 +762,10 @@ def __init__(self, in_dict, values=(), clear=False): self.clear = clear self._original = None - def __call__(self, f): if isinstance(f, class_types): return self.decorate_class(f) + @wraps(f) def _inner(*args, **kw): self._patch_dict() @@ -757,7 +776,6 @@ def _inner(*args, **kw): return _inner - def decorate_class(self, klass): for attr in dir(klass): attr_value = getattr(klass, attr) @@ -767,12 +785,10 @@ def decorate_class(self, klass): setattr(klass, attr, decorated) return klass - def __enter__(self): """Patch the dict.""" self._patch_dict() - def _patch_dict(self): """Unpatch the dict.""" values = self.values @@ -799,7 +815,6 @@ def _patch_dict(self): for key in values: in_dict[key] = values[key] - def _unpatch_dict(self): in_dict = self.in_dict original = self._original @@ -812,7 +827,6 @@ def _unpatch_dict(self): for key in original: in_dict[key] = original[key] - def __exit__(self, *args): self._unpatch_dict() return False @@ -846,71 +860,97 @@ def _clear_dict(in_dict): ) numerics = "add sub mul div truediv floordiv mod lshift rshift and xor or pow " -inplace = ' '.join('i%s' % n for n in numerics.split()) -right = ' '.join('r%s' % n for n in numerics.split()) -extra = '' +inplace = " ".join("i%s" % n for n in numerics.split()) +right = " ".join("r%s" % n for n in numerics.split()) +extra = "" if inPy3k: - extra = 'bool next ' + extra = "bool next " else: - extra = 'unicode long nonzero oct hex ' + extra = "unicode long nonzero oct hex " # __truediv__ and __rtruediv__ not available in Python 3 either # not including __prepare__, __instancecheck__, __subclasscheck__ # (as they are metaclass methods) # __del__ is not supported at all as it causes problems if it exists -_non_defaults = set('__%s__' % method for method in [ - 'cmp', 'getslice', 'setslice', 'coerce', 'subclasses', - 'dir', 'format', 'get', 'set', 'delete', 'reversed', - 'missing', 'reduce', 'reduce_ex', 'getinitargs', - 'getnewargs', 'getstate', 'setstate', 'getformat', - 'setformat', 'repr' -]) +_non_defaults = set( + "__%s__" % method + for method in [ + "cmp", + "getslice", + "setslice", + "coerce", + "subclasses", + "dir", + "format", + "get", + "set", + "delete", + "reversed", + "missing", + "reduce", + "reduce_ex", + "getinitargs", + "getnewargs", + "getstate", + "setstate", + "getformat", + "setformat", + "repr", + ] +) def _get_method(name, func): "Turns a callable object (like a mock) into a real function" + def method(self, *args, **kw): return func(self, *args, **kw) + method.__name__ = name return method _magics = set( - '__%s__' % method for method in - ' '.join([magic_methods, numerics, inplace, right, extra]).split() + "__%s__" % method + for method in " ".join([magic_methods, numerics, inplace, right, extra]).split() ) _all_magics = _magics | _non_defaults -_unsupported_magics = set([ - '__getattr__', '__setattr__', - '__init__', '__new__', '__prepare__' - '__instancecheck__', '__subclasscheck__', - '__del__' -]) +_unsupported_magics = set( + [ + "__getattr__", + "__setattr__", + "__init__", + "__new__", + "__prepare__" "__instancecheck__", + "__subclasscheck__", + "__del__", + ] +) _calculate_return_value = { - '__hash__': lambda self: object.__hash__(self), - '__str__': lambda self: object.__str__(self), - '__sizeof__': lambda self: object.__sizeof__(self), - '__unicode__': lambda self: unicode(object.__str__(self)), + "__hash__": lambda self: object.__hash__(self), + "__str__": lambda self: object.__str__(self), + "__sizeof__": lambda self: object.__sizeof__(self), + "__unicode__": lambda self: unicode(object.__str__(self)), } _return_values = { - '__int__': 1, - '__contains__': False, - '__len__': 0, - '__iter__': iter([]), - '__exit__': False, - '__complex__': 1j, - '__float__': 1.0, - '__bool__': True, - '__nonzero__': True, - '__oct__': '1', - '__hex__': '0x1', - '__long__': long(1), - '__index__': 1, + "__int__": 1, + "__contains__": False, + "__len__": 0, + "__iter__": iter([]), + "__exit__": False, + "__complex__": 1j, + "__float__": 1.0, + "__bool__": True, + "__nonzero__": True, + "__oct__": "1", + "__hex__": "0x1", + "__long__": long(1), + "__index__": 1, } @@ -938,6 +978,7 @@ class MagicMock(Mock): Attributes and the return value of a `MagicMock` will also be `MagicMocks`. """ + def __init__(self, *args, **kw): Mock.__init__(self, *args, **kw) diff --git a/tests/test_api.py b/tests/test_api.py index 9fbc7a678..0e611316a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -45,7 +45,7 @@ class TestShotgunApi(base.LiveTestBase): def setUp(self): super(TestShotgunApi, self).setUp() # give note unicode content - self.sg.update('Note', self.note['id'], {'content': u'La Pe\xf1a'}) + self.sg.update("Note", self.note["id"], {"content": "La Pe\xf1a"}) def test_info(self): """Called info""" @@ -55,9 +55,8 @@ def test_info(self): def test_server_dates(self): """Pass datetimes to the server""" # TODO check results - t = {'project': self.project, - 'start_date': datetime.date.today()} - self.sg.create('Task', t, ['content', 'sg_status_list']) + t = {"project": self.project, "start_date": datetime.date.today()} + self.sg.create("Task", t, ["content", "sg_status_list"]) def test_batch(self): """Batched create, update, delete""" @@ -66,39 +65,30 @@ def test_batch(self): { "request_type": "create", "entity_type": "Shot", - "data": { - "code": "New Shot 5", - "project": self.project - } + "data": {"code": "New Shot 5", "project": self.project}, }, { "request_type": "update", "entity_type": "Shot", - "entity_id": self.shot['id'], - "data": { - "code": "Changed 1" - } - } + "entity_id": self.shot["id"], + "data": {"code": "Changed 1"}, + }, ] new_shot, updated_shot = self.sg.batch(requests) - self.assertEqual(self.shot['id'], updated_shot["id"]) + self.assertEqual(self.shot["id"], updated_shot["id"]) self.assertTrue(new_shot.get("id")) new_shot_id = new_shot["id"] requests = [ - { - "request_type": "delete", - "entity_type": "Shot", - "entity_id": new_shot_id - }, + {"request_type": "delete", "entity_type": "Shot", "entity_id": new_shot_id}, { "request_type": "update", "entity_type": "Shot", - "entity_id": self.shot['id'], - "data": {"code": self.shot['code']} - } + "entity_id": self.shot["id"], + "data": {"code": self.shot["code"]}, + }, ] result = self.sg.batch(requests)[0] @@ -112,12 +102,12 @@ def test_empty_batch(self): def test_create_update_delete(self): """Called create, update, delete, revive""" data = { - 'project': self.project, - 'code': 'JohnnyApple_Design01_FaceFinal', - 'description': 'fixed rig per director final notes', - 'sg_status_list': 'rev', - 'entity': self.asset, - 'user': self.human_user + "project": self.project, + "code": "JohnnyApple_Design01_FaceFinal", + "description": "fixed rig per director final notes", + "sg_status_list": "rev", + "entity": self.asset, + "user": self.human_user, } version = self.sg.create("Version", data, return_fields=["id"]) @@ -126,9 +116,7 @@ def test_create_update_delete(self): # TODO check results more thoroughly # TODO: test returned fields are requested fields - data = data = { - "description": "updated test" - } + data = data = {"description": "updated test"} version = self.sg.update("Version", version["id"], data) self.assertTrue(isinstance(version, dict)) self.assertTrue("id" in version) @@ -144,9 +132,9 @@ def test_create_update_delete(self): self.assertEqual(False, rv) def test_last_accessed(self): - page = self.sg.find('Page', [], fields=['last_accessed'], limit=1) - self.assertEqual("Page", page[0]['type']) - self.assertEqual(datetime.datetime, type(page[0]['last_accessed'])) + page = self.sg.find("Page", [], fields=["last_accessed"], limit=1) + self.assertEqual("Page", page[0]["type"]) + self.assertEqual(datetime.datetime, type(page[0]["last_accessed"])) def test_get_session_token(self): """Got session UUID""" @@ -158,18 +146,23 @@ def test_upload_download(self): """Upload and download an attachment tests""" # upload / download only works against a live server because it does # not use the standard http interface - if 'localhost' in self.server_url: + if "localhost" in self.server_url: print("upload / down tests skipped for localhost") return this_dir, _ = os.path.split(__file__) - path = os.path.abspath(os.path.expanduser( - os.path.join(this_dir, "sg_logo.jpg"))) + path = os.path.abspath( + os.path.expanduser(os.path.join(this_dir, "sg_logo.jpg")) + ) size = os.stat(path).st_size - attach_id = self.sg.upload("Version", - self.version['id'], path, 'sg_uploaded_movie', - tag_list="monkeys, everywhere, send, help") + attach_id = self.sg.upload( + "Version", + self.version["id"], + path, + "sg_uploaded_movie", + tag_list="monkeys, everywhere, send, help", + ) # test download with attachment_id attach_file = self.sg.download_attachment(attach_id) @@ -186,23 +179,30 @@ def test_upload_download(self): self.assertEqual(orig_file, attach_file) # test download with attachment_id (write to disk) - file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sg_logo_download.jpg") + file_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "sg_logo_download.jpg" + ) result = self.sg.download_attachment(attach_id, file_path=file_path) self.assertEqual(result, file_path) # On windows read may not read to end of file unless opened 'rb' - fp = open(file_path, 'rb') + fp = open(file_path, "rb") attach_file = fp.read() fp.close() self.assertEqual(size, len(attach_file)) self.assertEqual(orig_file, attach_file) # test download with attachment hash - version = self.sg.find_one('Version', [['id', 'is', self.version['id']]], - ['sg_uploaded_movie']) + version = self.sg.find_one( + "Version", [["id", "is", self.version["id"]]], ["sg_uploaded_movie"] + ) # Look for the attachment we just uploaded, the attachments are not returned from latest # to earliest. - attachment = [v for k, v in version["sg_uploaded_movie"].items() if (k, v) == ("id", attach_id)] + attachment = [ + v + for k, v in version["sg_uploaded_movie"].items() + if (k, v) == ("id", attach_id) + ] self.assertEqual(len(attachment), 1) attachment = attachment[0] @@ -213,10 +213,9 @@ def test_upload_download(self): self.assertEqual(orig_file, attach_file) # test download with attachment hash (write to disk) - result = self.sg.download_attachment(attachment, - file_path=file_path) + result = self.sg.download_attachment(attachment, file_path=file_path) self.assertEqual(result, file_path) - fp = open(file_path, 'rb') + fp = open(file_path, "rb") attach_file = fp.read() fp.close() self.assertTrue(attach_file is not None) @@ -225,17 +224,23 @@ def test_upload_download(self): # test invalid requests INVALID_S3_URL = "https://sg-media-usor-01.s3.amazonaws.com/ada3de3ee3873875e1dd44f2eb0882c75ae36a4a/cd31346421dbeef781e0e480f259a3d36652d7f2/IMG_0465.MOV?AWSAccessKeyId=AKIAIQGOBSVN3FSQ5QFA&Expires=1371789959&Signature=SLbzv7DuVlZ8XAoOSQQAiGpF3u8%3D" # noqa - self.assertRaises(shotgun_api3.ShotgunFileDownloadError, - self.sg.download_attachment, - {"url": INVALID_S3_URL}) + self.assertRaises( + shotgun_api3.ShotgunFileDownloadError, + self.sg.download_attachment, + {"url": INVALID_S3_URL}, + ) INVALID_ATTACHMENT_ID = 99999999 - self.assertRaises(shotgun_api3.ShotgunFileDownloadError, - self.sg.download_attachment, - INVALID_ATTACHMENT_ID) - self.assertRaises(TypeError, self.sg.download_attachment, - "/path/to/some/file.jpg") - self.assertRaises(ValueError, self.sg.download_attachment, - {"id": 123, "type": "Shot"}) + self.assertRaises( + shotgun_api3.ShotgunFileDownloadError, + self.sg.download_attachment, + INVALID_ATTACHMENT_ID, + ) + self.assertRaises( + TypeError, self.sg.download_attachment, "/path/to/some/file.jpg" + ) + self.assertRaises( + ValueError, self.sg.download_attachment, {"id": 123, "type": "Shot"} + ) self.assertRaises(TypeError, self.sg.download_attachment) # test upload of non-ascii, unicode path @@ -251,10 +256,10 @@ def test_upload_download(self): # us up the way it used to. self.sg.upload( "Version", - self.version['id'], + self.version["id"], u_path, - 'sg_uploaded_movie', - tag_list="monkeys, everywhere, send, help" + "sg_uploaded_movie", + tag_list="monkeys, everywhere, send, help", ) # Also make sure that we can pass in a utf-8 encoded string path @@ -263,10 +268,10 @@ def test_upload_download(self): # situation as well as OS X and Linux. self.sg.upload( "Version", - self.version['id'], + self.version["id"], u_path.encode("utf-8"), - 'sg_uploaded_movie', - tag_list="monkeys, everywhere, send, help" + "sg_uploaded_movie", + tag_list="monkeys, everywhere, send, help", ) if six.PY2: # In Python2, make sure that non-utf-8 encoded paths raise when they @@ -280,26 +285,28 @@ def test_upload_download(self): file_path_s = os.path.join(this_dir, "./\xe3\x81\x94.shift-jis") file_path_u = file_path_s.decode("utf-8") - with open(file_path_u if sys.platform.startswith("win") else file_path_s, "w") as fh: + with open( + file_path_u if sys.platform.startswith("win") else file_path_s, "w" + ) as fh: fh.write("This is just a test file with some random data in it.") self.assertRaises( shotgun_api3.ShotgunError, self.sg.upload, "Version", - self.version['id'], + self.version["id"], file_path_u.encode("shift-jis"), - 'sg_uploaded_movie', - tag_list="monkeys, everywhere, send, help" + "sg_uploaded_movie", + tag_list="monkeys, everywhere, send, help", ) # But it should work in all cases if a unicode string is used. self.sg.upload( "Version", - self.version['id'], + self.version["id"], file_path_u, - 'sg_uploaded_movie', - tag_list="monkeys, everywhere, send, help" + "sg_uploaded_movie", + tag_list="monkeys, everywhere, send, help", ) # cleanup @@ -308,7 +315,7 @@ def test_upload_download(self): # cleanup os.remove(file_path) - @patch('shotgun_api3.Shotgun._send_form') + @patch("shotgun_api3.Shotgun._send_form") def test_upload_to_sg(self, mock_send_form): """ Upload an attachment tests for _upload_to_sg() @@ -324,24 +331,23 @@ def test_upload_to_sg(self, mock_send_form): ) upload_id = self.sg.upload( "Version", - self.version['id'], + self.version["id"], u_path, - 'attachments', - tag_list="monkeys, everywhere, send, help" + "attachments", + tag_list="monkeys, everywhere, send, help", ) mock_send_form_args, _ = mock_send_form.call_args display_name_to_send = mock_send_form_args[1].get("display_name", "") self.assertTrue(isinstance(upload_id, int)) self.assertFalse( - display_name_to_send.startswith("b'") and - display_name_to_send.endswith("'") + display_name_to_send.startswith("b'") and display_name_to_send.endswith("'") ) upload_id = self.sg.upload( "Version", - self.version['id'], + self.version["id"], u_path, - 'filmstrip_image', + "filmstrip_image", tag_list="monkeys, everywhere, send, help", ) self.assertTrue(isinstance(upload_id, int)) @@ -349,8 +355,7 @@ def test_upload_to_sg(self, mock_send_form): display_name_to_send = mock_send_form_args[1].get("display_name", "") self.assertTrue(isinstance(upload_id, int)) self.assertFalse( - display_name_to_send.startswith("b'") and - display_name_to_send.endswith("'") + display_name_to_send.startswith("b'") and display_name_to_send.endswith("'") ) mock_send_form.method.assert_called_once() @@ -359,23 +364,23 @@ def test_upload_to_sg(self, mock_send_form): shotgun_api3.ShotgunError, self.sg.upload, "Version", - self.version['id'], + self.version["id"], u_path, - 'attachments', - tag_list="monkeys, everywhere, send, help" + "attachments", + tag_list="monkeys, everywhere, send, help", ) self.sg.server_info["s3_direct_uploads_enabled"] = True def test_upload_thumbnail_in_create(self): """Upload a thumbnail via the create method""" this_dir, _ = os.path.split(__file__) - path = os.path.abspath(os.path.expanduser( - os.path.join(this_dir, "sg_logo.jpg"))) + path = os.path.abspath( + os.path.expanduser(os.path.join(this_dir, "sg_logo.jpg")) + ) # test thumbnail upload - data = {'image': path, 'code': 'Test Version', - 'project': self.project} - new_version = self.sg.create("Version", data, return_fields=['image']) + data = {"image": path, "code": "Test Version", "project": self.project} + new_version = self.sg.create("Version", data, return_fields=["image"]) new_version = self.find_one_await_thumbnail( "Version", [["id", "is", new_version["id"]]], @@ -384,104 +389,126 @@ def test_upload_thumbnail_in_create(self): self.assertTrue(new_version is not None) self.assertTrue(isinstance(new_version, dict)) - self.assertTrue(isinstance(new_version.get('id'), int)) - self.assertEqual(new_version.get('type'), 'Version') - self.assertEqual(new_version.get('project'), self.project) - self.assertTrue(new_version.get('image') is not None) + self.assertTrue(isinstance(new_version.get("id"), int)) + self.assertEqual(new_version.get("type"), "Version") + self.assertEqual(new_version.get("project"), self.project) + self.assertTrue(new_version.get("image") is not None) h = Http(".cache") - thumb_resp, content = h.request(new_version.get('image'), "GET") - self.assertIn(thumb_resp['status'], ['200', '304']) - self.assertIn(thumb_resp['content-type'], ['image/jpeg', 'image/png']) + thumb_resp, content = h.request(new_version.get("image"), "GET") + self.assertIn(thumb_resp["status"], ["200", "304"]) + self.assertIn(thumb_resp["content-type"], ["image/jpeg", "image/png"]) - self.sg.delete("Version", new_version['id']) + self.sg.delete("Version", new_version["id"]) # test filmstrip image upload - data = {'filmstrip_image': path, 'code': 'Test Version', - 'project': self.project} - new_version = self.sg.create("Version", data, return_fields=['filmstrip_image']) + data = { + "filmstrip_image": path, + "code": "Test Version", + "project": self.project, + } + new_version = self.sg.create("Version", data, return_fields=["filmstrip_image"]) self.assertTrue(new_version is not None) self.assertTrue(isinstance(new_version, dict)) - self.assertTrue(isinstance(new_version.get('id'), int)) - self.assertEqual(new_version.get('type'), 'Version') - self.assertEqual(new_version.get('project'), self.project) - self.assertTrue(new_version.get('filmstrip_image') is not None) + self.assertTrue(isinstance(new_version.get("id"), int)) + self.assertEqual(new_version.get("type"), "Version") + self.assertEqual(new_version.get("project"), self.project) + self.assertTrue(new_version.get("filmstrip_image") is not None) - url = new_version.get('filmstrip_image') - data = self.sg.download_attachment({'url': url}) + url = new_version.get("filmstrip_image") + data = self.sg.download_attachment({"url": url}) self.assertTrue(isinstance(data, six.binary_type)) - self.sg.delete("Version", new_version['id']) + self.sg.delete("Version", new_version["id"]) + # end test_upload_thumbnail_in_create def test_upload_thumbnail_for_version(self): """simple upload thumbnail for version test.""" this_dir, _ = os.path.split(__file__) - path = os.path.abspath(os.path.expanduser( - os.path.join(this_dir, "sg_logo.jpg"))) + path = os.path.abspath( + os.path.expanduser(os.path.join(this_dir, "sg_logo.jpg")) + ) # upload thumbnail - thumb_id = self.sg.upload_thumbnail("Version", self.version['id'], path) + thumb_id = self.sg.upload_thumbnail("Version", self.version["id"], path) self.assertTrue(isinstance(thumb_id, int)) # check result on version - version_with_thumbnail = self.sg.find_one('Version', [['id', 'is', self.version['id']]]) + version_with_thumbnail = self.sg.find_one( + "Version", [["id", "is", self.version["id"]]] + ) version_with_thumbnail = self.find_one_await_thumbnail( "Version", [["id", "is", self.version["id"]]] ) - self.assertEqual(version_with_thumbnail.get('type'), 'Version') - self.assertEqual(version_with_thumbnail.get('id'), self.version['id']) + self.assertEqual(version_with_thumbnail.get("type"), "Version") + self.assertEqual(version_with_thumbnail.get("id"), self.version["id"]) h = Http(".cache") - thumb_resp, content = h.request(version_with_thumbnail.get('image'), "GET") - self.assertIn(thumb_resp['status'], ['200', '304']) - self.assertIn(thumb_resp['content-type'], ['image/jpeg', 'image/png']) + thumb_resp, content = h.request(version_with_thumbnail.get("image"), "GET") + self.assertIn(thumb_resp["status"], ["200", "304"]) + self.assertIn(thumb_resp["content-type"], ["image/jpeg", "image/png"]) # clear thumbnail - response_clear_thumbnail = self.sg.update("Version", self.version['id'], {'image': None}) - expected_clear_thumbnail = {'id': self.version['id'], 'image': None, 'type': 'Version'} + response_clear_thumbnail = self.sg.update( + "Version", self.version["id"], {"image": None} + ) + expected_clear_thumbnail = { + "id": self.version["id"], + "image": None, + "type": "Version", + } self.assertEqual(expected_clear_thumbnail, response_clear_thumbnail) def test_upload_thumbnail_for_task(self): """simple upload thumbnail for task test.""" this_dir, _ = os.path.split(__file__) - path = os.path.abspath(os.path.expanduser( - os.path.join(this_dir, "sg_logo.jpg"))) + path = os.path.abspath( + os.path.expanduser(os.path.join(this_dir, "sg_logo.jpg")) + ) # upload thumbnail - thumb_id = self.sg.upload_thumbnail("Task", self.task['id'], path) + thumb_id = self.sg.upload_thumbnail("Task", self.task["id"], path) self.assertTrue(isinstance(thumb_id, int)) # check result on version - task_with_thumbnail = self.sg.find_one('Task', [['id', 'is', self.task['id']]]) + task_with_thumbnail = self.sg.find_one("Task", [["id", "is", self.task["id"]]]) task_with_thumbnail = self.find_one_await_thumbnail( "Task", [["id", "is", self.task["id"]]] ) - self.assertEqual(task_with_thumbnail.get('type'), 'Task') - self.assertEqual(task_with_thumbnail.get('id'), self.task['id']) + self.assertEqual(task_with_thumbnail.get("type"), "Task") + self.assertEqual(task_with_thumbnail.get("id"), self.task["id"]) h = Http(".cache") - thumb_resp, content = h.request(task_with_thumbnail.get('image'), "GET") - self.assertIn(thumb_resp['status'], ['200', '304']) - self.assertIn(thumb_resp['content-type'], ['image/jpeg', 'image/png']) + thumb_resp, content = h.request(task_with_thumbnail.get("image"), "GET") + self.assertIn(thumb_resp["status"], ["200", "304"]) + self.assertIn(thumb_resp["content-type"], ["image/jpeg", "image/png"]) # clear thumbnail - response_clear_thumbnail = self.sg.update("Version", self.version['id'], {'image': None}) - expected_clear_thumbnail = {'id': self.version['id'], 'image': None, 'type': 'Version'} + response_clear_thumbnail = self.sg.update( + "Version", self.version["id"], {"image": None} + ) + expected_clear_thumbnail = { + "id": self.version["id"], + "image": None, + "type": "Version", + } self.assertEqual(expected_clear_thumbnail, response_clear_thumbnail) def test_upload_thumbnail_with_upload_function(self): """Upload thumbnail via upload function test""" - path = os.path.abspath(os.path.expanduser(os.path.join(os.path.dirname(__file__), "sg_logo.jpg"))) + path = os.path.abspath( + os.path.expanduser(os.path.join(os.path.dirname(__file__), "sg_logo.jpg")) + ) # upload thumbnail - thumb_id = self.sg.upload("Task", self.task['id'], path, 'image') + thumb_id = self.sg.upload("Task", self.task["id"], path, "image") self.assertTrue(isinstance(thumb_id, int)) # upload filmstrip thumbnail - f_thumb_id = self.sg.upload("Task", self.task['id'], path, 'filmstrip_image') + f_thumb_id = self.sg.upload("Task", self.task["id"], path, "filmstrip_image") self.assertTrue(isinstance(f_thumb_id, int)) def test_requires_direct_s3_upload(self): @@ -494,19 +521,25 @@ def test_requires_direct_s3_upload(self): self.sg.server_info["s3_direct_uploads_enabled"] = None # Test s3_enabled_upload_types and s3_direct_uploads_enabled not set - self.assertFalse(self.sg._requires_direct_s3_upload("Version", "sg_uploaded_movie")) + self.assertFalse( + self.sg._requires_direct_s3_upload("Version", "sg_uploaded_movie") + ) self.sg.server_info["s3_enabled_upload_types"] = { "Version": ["sg_uploaded_movie"] } # Test direct_uploads_enabled not set - self.assertFalse(self.sg._requires_direct_s3_upload("Version", "sg_uploaded_movie")) + self.assertFalse( + self.sg._requires_direct_s3_upload("Version", "sg_uploaded_movie") + ) self.sg.server_info["s3_direct_uploads_enabled"] = True # Test regular path - self.assertTrue(self.sg._requires_direct_s3_upload("Version", "sg_uploaded_movie")) + self.assertTrue( + self.sg._requires_direct_s3_upload("Version", "sg_uploaded_movie") + ) self.assertFalse(self.sg._requires_direct_s3_upload("Version", "abc")) self.assertFalse(self.sg._requires_direct_s3_upload("Abc", "abc")) @@ -514,10 +547,12 @@ def test_requires_direct_s3_upload(self): self.sg.server_info["s3_enabled_upload_types"] = { "Version": ["sg_uploaded_movie", "test", "other"], "Test": ["*"], - "Asset": "*" + "Asset": "*", } - self.assertTrue(self.sg._requires_direct_s3_upload("Version", "sg_uploaded_movie")) + self.assertTrue( + self.sg._requires_direct_s3_upload("Version", "sg_uploaded_movie") + ) self.assertTrue(self.sg._requires_direct_s3_upload("Version", "test")) self.assertTrue(self.sg._requires_direct_s3_upload("Version", "other")) self.assertTrue(self.sg._requires_direct_s3_upload("Test", "abc")) @@ -525,22 +560,26 @@ def test_requires_direct_s3_upload(self): # Test default allowed upload type self.sg.server_info["s3_enabled_upload_types"] = None - self.assertTrue(self.sg._requires_direct_s3_upload("Version", "sg_uploaded_movie")) + self.assertTrue( + self.sg._requires_direct_s3_upload("Version", "sg_uploaded_movie") + ) self.assertFalse(self.sg._requires_direct_s3_upload("Version", "test")) # Test star entity_type self.sg.server_info["s3_enabled_upload_types"] = { "*": ["sg_uploaded_movie", "test"] } - self.assertTrue(self.sg._requires_direct_s3_upload("Something", "sg_uploaded_movie")) + self.assertTrue( + self.sg._requires_direct_s3_upload("Something", "sg_uploaded_movie") + ) self.assertTrue(self.sg._requires_direct_s3_upload("Version", "test")) self.assertFalse(self.sg._requires_direct_s3_upload("Version", "other")) # Test entity_type and field_name wildcard - self.sg.server_info["s3_enabled_upload_types"] = { - "*": "*" - } - self.assertTrue(self.sg._requires_direct_s3_upload("Something", "sg_uploaded_movie")) + self.sg.server_info["s3_enabled_upload_types"] = {"*": "*"} + self.assertTrue( + self.sg._requires_direct_s3_upload("Something", "sg_uploaded_movie") + ) self.assertTrue(self.sg._requires_direct_s3_upload("Version", "abc")) self.sg.server_info["s3_enabled_upload_types"] = upload_types @@ -548,10 +587,13 @@ def test_requires_direct_s3_upload(self): def test_linked_thumbnail_url(self): this_dir, _ = os.path.split(__file__) - path = os.path.abspath(os.path.expanduser( - os.path.join(this_dir, "sg_logo.jpg"))) + path = os.path.abspath( + os.path.expanduser(os.path.join(this_dir, "sg_logo.jpg")) + ) - thumb_id = self.sg.upload_thumbnail("Project", self.version['project']['id'], path) + thumb_id = self.sg.upload_thumbnail( + "Project", self.version["project"]["id"], path + ) response_version_with_project = self.find_one_await_thumbnail( "Version", @@ -562,23 +604,31 @@ def test_linked_thumbnail_url(self): if self.sg.server_caps.version and self.sg.server_caps.version >= (3, 3, 0): - self.assertEqual(response_version_with_project.get('type'), 'Version') - self.assertEqual(response_version_with_project.get('id'), self.version['id']) - self.assertEqual(response_version_with_project.get('code'), self.config.version_code) + self.assertEqual(response_version_with_project.get("type"), "Version") + self.assertEqual( + response_version_with_project.get("id"), self.version["id"] + ) + self.assertEqual( + response_version_with_project.get("code"), self.config.version_code + ) h = Http(".cache") - thumb_resp, content = h.request(response_version_with_project.get('project.Project.image'), "GET") - self.assertIn(thumb_resp['status'], ['200', '304']) - self.assertIn(thumb_resp['content-type'], ['image/jpeg', 'image/png']) + thumb_resp, content = h.request( + response_version_with_project.get("project.Project.image"), "GET" + ) + self.assertIn(thumb_resp["status"], ["200", "304"]) + self.assertIn(thumb_resp["content-type"], ["image/jpeg", "image/png"]) else: expected_version_with_project = { - 'code': self.config.version_code, - 'type': 'Version', - 'id': self.version['id'], - 'project.Project.image': thumb_id + "code": self.config.version_code, + "type": "Version", + "id": self.version["id"], + "project.Project.image": thumb_id, } - self.assertEqual(expected_version_with_project, response_version_with_project) + self.assertEqual( + expected_version_with_project, response_version_with_project + ) # For now skip tests that are erroneously failling on some sites to # allow CI to pass until the known issue causing this is resolved. @@ -601,49 +651,51 @@ def share_thumbnail_retry(*args, **kwargs): return thumbnail_id this_dir, _ = os.path.split(__file__) - path = os.path.abspath(os.path.expanduser(os.path.join(this_dir, "sg_logo.jpg"))) + path = os.path.abspath( + os.path.expanduser(os.path.join(this_dir, "sg_logo.jpg")) + ) # upload thumbnail to first entity and share it with the rest share_thumbnail_retry([self.version, self.shot], thumbnail_path=path) response_version_thumbnail = self.find_one_await_thumbnail( - 'Version', - [['id', 'is', self.version['id']]], - fields=['id', 'code', 'image'], + "Version", + [["id", "is", self.version["id"]]], + fields=["id", "code", "image"], ) response_shot_thumbnail = self.find_one_await_thumbnail( - 'Shot', - [['id', 'is', self.shot['id']]], - fields=['id', 'code', 'image'], + "Shot", + [["id", "is", self.shot["id"]]], + fields=["id", "code", "image"], ) - shot_url = urllib.parse.urlparse(response_shot_thumbnail.get('image')) - version_url = urllib.parse.urlparse(response_version_thumbnail.get('image')) + shot_url = urllib.parse.urlparse(response_shot_thumbnail.get("image")) + version_url = urllib.parse.urlparse(response_version_thumbnail.get("image")) shot_path = _get_path(shot_url) version_path = _get_path(version_url) self.assertEqual(shot_path, version_path) # share thumbnail from source entity with entities - self.sg.upload_thumbnail("Version", self.version['id'], path) + self.sg.upload_thumbnail("Version", self.version["id"], path) share_thumbnail_retry([self.asset, self.shot], source_entity=self.version) response_version_thumbnail = self.find_one_await_thumbnail( - 'Version', - [['id', 'is', self.version['id']]], - fields=['id', 'code', 'image'], + "Version", + [["id", "is", self.version["id"]]], + fields=["id", "code", "image"], ) response_shot_thumbnail = self.find_one_await_thumbnail( - 'Shot', - [['id', 'is', self.shot['id']]], - fields=['id', 'code', 'image'], + "Shot", + [["id", "is", self.shot["id"]]], + fields=["id", "code", "image"], ) response_asset_thumbnail = self.find_one_await_thumbnail( - 'Asset', - [['id', 'is', self.asset['id']]], - fields=['id', 'code', 'image'], + "Asset", + [["id", "is", self.asset["id"]]], + fields=["id", "code", "image"], ) - shot_url = urllib.parse.urlparse(response_shot_thumbnail.get('image')) - version_url = urllib.parse.urlparse(response_version_thumbnail.get('image')) - asset_url = urllib.parse.urlparse(response_asset_thumbnail.get('image')) + shot_url = urllib.parse.urlparse(response_shot_thumbnail.get("image")) + version_url = urllib.parse.urlparse(response_version_thumbnail.get("image")) + asset_url = urllib.parse.urlparse(response_asset_thumbnail.get("image")) shot_path = _get_path(shot_url) version_path = _get_path(version_url) @@ -653,32 +705,48 @@ def share_thumbnail_retry(*args, **kwargs): self.assertEqual(version_path, asset_path) # raise errors when missing required params or providing conflicting ones - self.assertRaises(shotgun_api3.ShotgunError, self.sg.share_thumbnail, - [self.shot, self.asset], path, self.version) - self.assertRaises(shotgun_api3.ShotgunError, self.sg.share_thumbnail, - [self.shot, self.asset]) + self.assertRaises( + shotgun_api3.ShotgunError, + self.sg.share_thumbnail, + [self.shot, self.asset], + path, + self.version, + ) + self.assertRaises( + shotgun_api3.ShotgunError, self.sg.share_thumbnail, [self.shot, self.asset] + ) - @patch('shotgun_api3.Shotgun._send_form') + @patch("shotgun_api3.Shotgun._send_form") def test_share_thumbnail_not_ready(self, mock_send_form): """throw an exception if trying to share a transient thumbnail""" mock_send_form.method.assert_called_once() - mock_send_form.return_value = ("2" - "\nsource_entity image is a transient thumbnail that cannot be shared. " - "Try again later, when the final thumbnail is available\n") + mock_send_form.return_value = ( + "2" + "\nsource_entity image is a transient thumbnail that cannot be shared. " + "Try again later, when the final thumbnail is available\n" + ) - self.assertRaises(shotgun_api3.ShotgunThumbnailNotReady, self.sg.share_thumbnail, - [self.version, self.shot], source_entity=self.asset) + self.assertRaises( + shotgun_api3.ShotgunThumbnailNotReady, + self.sg.share_thumbnail, + [self.version, self.shot], + source_entity=self.asset, + ) - @patch('shotgun_api3.Shotgun._send_form') + @patch("shotgun_api3.Shotgun._send_form") def test_share_thumbnail_returns_error(self, mock_send_form): """throw an exception if server returns an error code""" mock_send_form.method.assert_called_once() mock_send_form.return_value = "1\nerror message.\n" - self.assertRaises(shotgun_api3.ShotgunError, self.sg.share_thumbnail, - [self.version, self.shot], source_entity=self.asset) + self.assertRaises( + shotgun_api3.ShotgunError, + self.sg.share_thumbnail, + [self.version, self.shot], + source_entity=self.asset, + ) def test_deprecated_functions(self): """Deprecated functions raise errors""" @@ -687,35 +755,36 @@ def test_deprecated_functions(self): def test_simple_summary(self): """Test simple call to summarize""" - summaries = [{'field': 'id', 'type': 'count'}] - grouping = [{'direction': 'asc', 'field': 'id', 'type': 'exact'}] - filters = [['project', 'is', self.project]] - result = self.sg.summarize('Shot', - filters=filters, - summary_fields=summaries, - grouping=grouping) - assert(result['groups']) - assert(result['groups'][0]['group_name']) - assert(result['groups'][0]['group_value']) - assert(result['groups'][0]['summaries']) - assert(result['summaries']) + summaries = [{"field": "id", "type": "count"}] + grouping = [{"direction": "asc", "field": "id", "type": "exact"}] + filters = [["project", "is", self.project]] + result = self.sg.summarize( + "Shot", filters=filters, summary_fields=summaries, grouping=grouping + ) + assert result["groups"] + assert result["groups"][0]["group_name"] + assert result["groups"][0]["group_value"] + assert result["groups"][0]["summaries"] + assert result["summaries"] def test_summary_include_archived_projects(self): """Test summarize with archived project""" if self.sg.server_caps.version > (5, 3, 13): # archive project - self.sg.update('Project', self.project['id'], {'archived': True}) + self.sg.update("Project", self.project["id"], {"archived": True}) # Ticket #25082 ability to hide archived projects in summary - summaries = [{'field': 'id', 'type': 'count'}] - grouping = [{'direction': 'asc', 'field': 'id', 'type': 'exact'}] - filters = [['project', 'is', self.project]] - result = self.sg.summarize('Shot', - filters=filters, - summary_fields=summaries, - grouping=grouping, - include_archived_projects=False) - self.assertEqual(result['summaries']['id'], 0) - self.sg.update('Project', self.project['id'], {'archived': False}) + summaries = [{"field": "id", "type": "count"}] + grouping = [{"direction": "asc", "field": "id", "type": "exact"}] + filters = [["project", "is", self.project]] + result = self.sg.summarize( + "Shot", + filters=filters, + summary_fields=summaries, + grouping=grouping, + include_archived_projects=False, + ) + self.assertEqual(result["summaries"]["id"], 0) + self.sg.update("Project", self.project["id"], {"archived": False}) def test_summary_values(self): """Test summarize return data""" @@ -729,28 +798,28 @@ def test_summary_values(self): "code": "%s Shot 1" % shot_prefix, "sg_status_list": "ip", "sg_cut_duration": 100, - "project": self.project + "project": self.project, } shot_data_2 = { "code": "%s Shot 2" % shot_prefix, "sg_status_list": "ip", "sg_cut_duration": 100, - "project": self.project + "project": self.project, } shot_data_3 = { "code": "%s Shot 3" % shot_prefix, "sg_status_list": "fin", "sg_cut_duration": 100, - "project": self.project + "project": self.project, } shot_data_4 = { "code": "%s Shot 4" % shot_prefix, "sg_status_list": "wtg", "sg_cut_duration": 0, - "project": self.project + "project": self.project, } shots.append(self.sg.create("Shot", shot_data_1)) @@ -758,140 +827,167 @@ def test_summary_values(self): shots.append(self.sg.create("Shot", shot_data_3)) shots.append(self.sg.create("Shot", shot_data_4)) - summaries = [{'field': 'id', 'type': 'count'}, - {'field': 'sg_cut_duration', 'type': 'sum'}] - grouping = [{'direction': 'asc', - 'field': 'sg_status_list', - 'type': 'exact'}] - filters = [['project', 'is', self.project], - ['code', 'starts_with', shot_prefix]] - result = self.sg.summarize('Shot', - filters=filters, - summary_fields=summaries, - grouping=grouping) - count = {'id': 4, 'sg_cut_duration': 300} + summaries = [ + {"field": "id", "type": "count"}, + {"field": "sg_cut_duration", "type": "sum"}, + ] + grouping = [{"direction": "asc", "field": "sg_status_list", "type": "exact"}] + filters = [ + ["project", "is", self.project], + ["code", "starts_with", shot_prefix], + ] + result = self.sg.summarize( + "Shot", filters=filters, summary_fields=summaries, grouping=grouping + ) + count = {"id": 4, "sg_cut_duration": 300} groups = [ { - 'group_name': 'fin', - 'group_value': 'fin', - 'summaries': {'id': 1, 'sg_cut_duration': 100} + "group_name": "fin", + "group_value": "fin", + "summaries": {"id": 1, "sg_cut_duration": 100}, }, { - 'group_name': 'ip', - 'group_value': 'ip', - 'summaries': {'id': 2, 'sg_cut_duration': 200} + "group_name": "ip", + "group_value": "ip", + "summaries": {"id": 2, "sg_cut_duration": 200}, }, { - 'group_name': 'wtg', - 'group_value': 'wtg', - 'summaries': {'id': 1, 'sg_cut_duration': 0} - } + "group_name": "wtg", + "group_value": "wtg", + "summaries": {"id": 1, "sg_cut_duration": 0}, + }, ] # clean up batch_data = [] for s in shots: - batch_data.append({ - "request_type": "delete", - "entity_type": "Shot", - "entity_id": s["id"] - }) + batch_data.append( + {"request_type": "delete", "entity_type": "Shot", "entity_id": s["id"]} + ) self.sg.batch(batch_data) - self.assertEqual(result['summaries'], count) + self.assertEqual(result["summaries"], count) # Do not assume the order of the summarized results. self.assertEqual( - sorted( - result['groups'], - key=lambda x: x["group_name"] - ), - groups + sorted(result["groups"], key=lambda x: x["group_name"]), groups ) def test_ensure_ascii(self): - '''test_ensure_ascii tests ensure_unicode flag.''' - sg_ascii = shotgun_api3.Shotgun(self.config.server_url, - ensure_ascii=True, - **self.auth_args) + """test_ensure_ascii tests ensure_unicode flag.""" + sg_ascii = shotgun_api3.Shotgun( + self.config.server_url, ensure_ascii=True, **self.auth_args + ) - result = sg_ascii.find_one('Note', [['id', 'is', self.note['id']]], fields=['content']) + result = sg_ascii.find_one( + "Note", [["id", "is", self.note["id"]]], fields=["content"] + ) if six.PY2: # In Python3 there isn't a separate unicode type. self.assertFalse(_has_unicode(result)) def test_ensure_unicode(self): - '''test_ensure_unicode tests ensure_unicode flag.''' - sg_unicode = shotgun_api3.Shotgun(self.config.server_url, - ensure_ascii=False, - **self.auth_args) - result = sg_unicode.find_one('Note', [['id', 'is', self.note['id']]], fields=['content']) + """test_ensure_unicode tests ensure_unicode flag.""" + sg_unicode = shotgun_api3.Shotgun( + self.config.server_url, ensure_ascii=False, **self.auth_args + ) + result = sg_unicode.find_one( + "Note", [["id", "is", self.note["id"]]], fields=["content"] + ) self.assertTrue(_has_unicode(result)) def test_work_schedule(self): - '''test_work_schedule tests WorkDayRules api''' + """test_work_schedule tests WorkDayRules api""" self.maxDiff = None - start_date = '2012-01-01' + start_date = "2012-01-01" start_date_obj = datetime.datetime(2012, 1, 1) - end_date = '2012-01-07' + end_date = "2012-01-07" end_date_obj = datetime.datetime(2012, 1, 7) project = self.project # We're going to be comparing this value with the value returned from the server, so extract only the type, id # and name - user = {"type": self.human_user["type"], "id": self.human_user["id"], "name": self.human_user["name"]} + user = { + "type": self.human_user["type"], + "id": self.human_user["id"], + "name": self.human_user["name"], + } work_schedule = self.sg.work_schedule_read(start_date, end_date, project, user) # Test that the work_schedule_read api method is called with the 'start_date' and 'end_date' arguments # in the 'YYYY-MM-DD' string format. - self.assertRaises(shotgun_api3.ShotgunError, self.sg.work_schedule_read, - start_date_obj, end_date_obj, project, user) + self.assertRaises( + shotgun_api3.ShotgunError, + self.sg.work_schedule_read, + start_date_obj, + end_date_obj, + project, + user, + ) - resp = self.sg.work_schedule_update('2012-01-02', False, 'Studio Holiday') + resp = self.sg.work_schedule_update("2012-01-02", False, "Studio Holiday") expected = { - 'date': '2012-01-02', - 'description': 'Studio Holiday', - 'project': None, - 'user': None, - 'working': False + "date": "2012-01-02", + "description": "Studio Holiday", + "project": None, + "user": None, + "working": False, } self.assertEqual(expected, resp) resp = self.sg.work_schedule_read(start_date, end_date, project, user) - work_schedule['2012-01-02'] = {"reason": "STUDIO_EXCEPTION", "working": False, "description": "Studio Holiday"} + work_schedule["2012-01-02"] = { + "reason": "STUDIO_EXCEPTION", + "working": False, + "description": "Studio Holiday", + } self.assertEqual(work_schedule, resp) - resp = self.sg.work_schedule_update('2012-01-03', False, 'Project Holiday', project) + resp = self.sg.work_schedule_update( + "2012-01-03", False, "Project Holiday", project + ) expected = { - 'date': '2012-01-03', - 'description': 'Project Holiday', - 'project': project, - 'user': None, - 'working': False + "date": "2012-01-03", + "description": "Project Holiday", + "project": project, + "user": None, + "working": False, } self.assertEqual(expected, resp) resp = self.sg.work_schedule_read(start_date, end_date, project, user) - work_schedule['2012-01-03'] = { + work_schedule["2012-01-03"] = { "reason": "PROJECT_EXCEPTION", "working": False, - "description": "Project Holiday" + "description": "Project Holiday", } self.assertEqual(work_schedule, resp) jan4 = datetime.datetime(2012, 1, 4) - self.assertRaises(shotgun_api3.ShotgunError, self.sg.work_schedule_update, - jan4, False, 'Artist Holiday', user=user) + self.assertRaises( + shotgun_api3.ShotgunError, + self.sg.work_schedule_update, + jan4, + False, + "Artist Holiday", + user=user, + ) - resp = self.sg.work_schedule_update("2012-01-04", False, 'Artist Holiday', user=user) + resp = self.sg.work_schedule_update( + "2012-01-04", False, "Artist Holiday", user=user + ) expected = { - 'date': '2012-01-04', - 'description': 'Artist Holiday', - 'project': None, - 'user': user, - 'working': False + "date": "2012-01-04", + "description": "Artist Holiday", + "project": None, + "user": user, + "working": False, } self.assertEqual(expected, resp) resp = self.sg.work_schedule_read(start_date, end_date, project, user) - work_schedule['2012-01-04'] = {"reason": "USER_EXCEPTION", "working": False, "description": "Artist Holiday"} + work_schedule["2012-01-04"] = { + "reason": "USER_EXCEPTION", + "working": False, + "description": "Artist Holiday", + } self.assertEqual(work_schedule, resp) # test_preferences_read fails when preferences don't match the expected @@ -908,21 +1004,21 @@ def test_preferences_read(self): resp = self.sg.preferences_read() expected = { - 'date_component_order': 'month_day', - 'duration_units': 'days', - 'format_currency_fields_decimal_options': '$1,000.99', - 'format_currency_fields_display_dollar_sign': False, - 'format_currency_fields_negative_options': '- $1,000', - 'format_date_fields': '08/04/22 OR 04/08/22 (depending on the Month order preference)', - 'format_float_fields': '9,999.99', - 'format_float_fields_rounding': '9.999999', - 'format_footage_fields': '10-05', - 'format_number_fields': '1,000', - 'format_time_hour_fields': '12 hour', - 'hours_per_day': 8.0, - 'support_local_storage': True, - 'enable_rv_integration': True, - 'enable_shotgun_review_for_rv': False, + "date_component_order": "month_day", + "duration_units": "days", + "format_currency_fields_decimal_options": "$1,000.99", + "format_currency_fields_display_dollar_sign": False, + "format_currency_fields_negative_options": "- $1,000", + "format_date_fields": "08/04/22 OR 04/08/22 (depending on the Month order preference)", + "format_float_fields": "9,999.99", + "format_float_fields_rounding": "9.999999", + "format_footage_fields": "10-05", + "format_number_fields": "1,000", + "format_time_hour_fields": "12 hour", + "hours_per_day": 8.0, + "support_local_storage": True, + "enable_rv_integration": True, + "enable_shotgun_review_for_rv": False, } # Simply make sure viewmaster settings are there. These change frequently and we # don't want to have the test break because Viewmaster changed or because we didn't @@ -933,253 +1029,238 @@ def test_preferences_read(self): self.assertEqual(expected, resp) # all filtered - resp = self.sg.preferences_read(['date_component_order', 'support_local_storage']) + resp = self.sg.preferences_read( + ["date_component_order", "support_local_storage"] + ) - expected = { - 'date_component_order': 'month_day', - 'support_local_storage': True - } + expected = {"date_component_order": "month_day", "support_local_storage": True} self.assertEqual(expected, resp) # all filtered with invalid pref - resp = self.sg.preferences_read(['date_component_order', 'support_local_storage', 'email_notifications']) + resp = self.sg.preferences_read( + ["date_component_order", "support_local_storage", "email_notifications"] + ) - expected = { - 'date_component_order': 'month_day', - 'support_local_storage': True - } + expected = {"date_component_order": "month_day", "support_local_storage": True} self.assertEqual(expected, resp) class TestDataTypes(base.LiveTestBase): - '''Test fields representing the different data types mapped on the server side. + """Test fields representing the different data types mapped on the server side. - Untested data types: password, percent, pivot_column, serializable, image, currency - system_task_type, timecode, url, uuid, url_template - ''' + Untested data types: password, percent, pivot_column, serializable, image, currency + system_task_type, timecode, url, uuid, url_template + """ def setUp(self): super(TestDataTypes, self).setUp() def test_set_checkbox(self): - entity = 'HumanUser' - entity_id = self.human_user['id'] - field_name = 'email_notes' + entity = "HumanUser" + entity_id = self.human_user["id"] + field_name = "email_notes" pos_values = [False, True] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) def test_set_color(self): - entity = 'Task' - entity_id = self.task['id'] - field_name = 'color' - pos_values = ['pipeline_step', '222,0,0'] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + entity = "Task" + entity_id = self.task["id"] + field_name = "color" + pos_values = ["pipeline_step", "222,0,0"] + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) def test_set_date(self): - entity = 'Task' - entity_id = self.task['id'] - field_name = 'due_date' - pos_values = ['2008-05-08', '2011-05-05'] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + entity = "Task" + entity_id = self.task["id"] + field_name = "due_date" + pos_values = ["2008-05-08", "2011-05-05"] + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) def test_set_date_time(self): if self.config.jenkins: self.skipTest("Jenkins. locked_until not updating.") - entity = 'HumanUser' - entity_id = self.human_user['id'] - field_name = 'locked_until' + entity = "HumanUser" + entity_id = self.human_user["id"] + field_name = "locked_until" local = shotgun_api3.shotgun.SG_TIMEZONE.local dt_1 = datetime.datetime(2008, 10, 13, 23, 10, tzinfo=local) dt_2 = datetime.datetime(2009, 10, 13, 23, 10, tzinfo=local) pos_values = [dt_1, dt_2] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) def test_set_duration(self): - entity = 'Task' - entity_id = self.task['id'] - field_name = 'duration' + entity = "Task" + entity_id = self.task["id"] + field_name = "duration" pos_values = [2100, 1300] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) def test_set_entity(self): - entity = 'Task' - entity_id = self.task['id'] - field_name = 'entity' + entity = "Task" + entity_id = self.task["id"] + field_name = "entity" pos_values = [self.asset, self.shot] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) - self.assertEqual(expected['id'], actual['id']) + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) + self.assertEqual(expected["id"], actual["id"]) def test_set_float(self): - entity = 'Version' - entity_id = self.version['id'] - field_name = 'sg_movie_aspect_ratio' + entity = "Version" + entity_id = self.version["id"] + field_name = "sg_movie_aspect_ratio" pos_values = [2.0, 3.0] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) def test_set_list(self): - entity = 'Note' - entity_id = self.note['id'] - field_name = 'sg_note_type' - pos_values = ['Internal', 'Client'] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + entity = "Note" + entity_id = self.note["id"] + field_name = "sg_note_type" + pos_values = ["Internal", "Client"] + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) def test_set_multi_entity(self): - sg = shotgun_api3.Shotgun(self.config.server_url, - **self.auth_args) - keys = ['project', 'user', 'code'] - data = {'project': self.project, - 'user': self.human_user, - 'code': 'Alpha'} - version_1 = base._find_or_create_entity(sg, 'Version', data, keys) - data = {'project': self.project, - 'user': self.human_user, - 'code': 'Beta'} - version_2 = base._find_or_create_entity(sg, 'Version', data, keys) - - entity = 'Playlist' - entity_id = self.playlist['id'] - field_name = 'versions' + sg = shotgun_api3.Shotgun(self.config.server_url, **self.auth_args) + keys = ["project", "user", "code"] + data = {"project": self.project, "user": self.human_user, "code": "Alpha"} + version_1 = base._find_or_create_entity(sg, "Version", data, keys) + data = {"project": self.project, "user": self.human_user, "code": "Beta"} + version_2 = base._find_or_create_entity(sg, "Version", data, keys) + + entity = "Playlist" + entity_id = self.playlist["id"] + field_name = "versions" # Default set behaviour pos_values = [[version_1, version_2]] - expected, actual = self.assert_set_field(entity, entity_id, field_name, pos_values) + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(len(expected), len(actual)) self.assertEqual( - sorted([x['id'] for x in expected]), - sorted([x['id'] for x in actual]) + sorted([x["id"] for x in expected]), sorted([x["id"] for x in actual]) ) # Multi-entity remove mode pos_values = [[version_1]] - expected, actual = self.assert_set_field(entity, entity_id, field_name, pos_values, - multi_entity_update_mode='remove') + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values, multi_entity_update_mode="remove" + ) self.assertEqual(1, len(actual)) self.assertEqual(len(expected), len(actual)) - self.assertNotEqual(expected[0]['id'], actual[0]['id']) - self.assertEqual(version_2['id'], actual[0]['id']) + self.assertNotEqual(expected[0]["id"], actual[0]["id"]) + self.assertEqual(version_2["id"], actual[0]["id"]) # Multi-entity add mode pos_values = [[version_1]] - expected, actual = self.assert_set_field(entity, entity_id, field_name, pos_values, - multi_entity_update_mode='add') + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values, multi_entity_update_mode="add" + ) self.assertEqual(2, len(actual)) - self.assertTrue(version_1['id'] in [x['id'] for x in actual]) + self.assertTrue(version_1["id"] in [x["id"] for x in actual]) # Multi-entity set mode pos_values = [[version_1, version_2]] - expected, actual = self.assert_set_field(entity, entity_id, field_name, pos_values, - multi_entity_update_mode='set') + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values, multi_entity_update_mode="set" + ) self.assertEqual(len(expected), len(actual)) self.assertEqual( - sorted([x['id'] for x in expected]), - sorted([x['id'] for x in actual]) + sorted([x["id"] for x in expected]), sorted([x["id"] for x in actual]) ) def test_set_number(self): - entity = 'Shot' - entity_id = self.shot['id'] - field_name = 'head_in' + entity = "Shot" + entity_id = self.shot["id"] + field_name = "head_in" pos_values = [2300, 1300] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) def test_set_status_list(self): - entity = 'Task' - entity_id = self.task['id'] - field_name = 'sg_status_list' - pos_values = ['wtg', 'fin'] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + entity = "Task" + entity_id = self.task["id"] + field_name = "sg_status_list" + pos_values = ["wtg", "fin"] + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) def test_set_tag_list(self): - entity = 'Task' - entity_id = self.task['id'] - field_name = 'tag_list' - pos_values = [['a', 'b'], ['c']] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + entity = "Task" + entity_id = self.task["id"] + field_name = "tag_list" + pos_values = [["a", "b"], ["c"]] + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) def test_set_text(self): - entity = 'Note' - entity_id = self.note['id'] - field_name = 'content' - pos_values = ['this content', 'that content'] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + entity = "Note" + entity_id = self.note["id"] + field_name = "content" + pos_values = ["this content", "that content"] + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) def test_set_text_html_entity(self): - entity = 'Note' - entity_id = self.note['id'] - field_name = 'content' - pos_values = ['<', '<'] - expected, actual = self.assert_set_field(entity, - entity_id, - field_name, - pos_values) + entity = "Note" + entity_id = self.note["id"] + field_name = "content" + pos_values = ["<", "<"] + expected, actual = self.assert_set_field( + entity, entity_id, field_name, pos_values + ) self.assertEqual(expected, actual) - def assert_set_field(self, entity, entity_id, field_name, pos_values, multi_entity_update_mode=None): - query_result = self.sg.find_one(entity, - [['id', 'is', entity_id]], - [field_name]) + def assert_set_field( + self, entity, entity_id, field_name, pos_values, multi_entity_update_mode=None + ): + query_result = self.sg.find_one(entity, [["id", "is", entity_id]], [field_name]) initial_value = query_result[field_name] new_value = (initial_value == pos_values[0] and pos_values[1]) or pos_values[0] if multi_entity_update_mode: - self.sg.update(entity, entity_id, {field_name: new_value}, - multi_entity_update_modes={field_name: multi_entity_update_mode}) + self.sg.update( + entity, + entity_id, + {field_name: new_value}, + multi_entity_update_modes={field_name: multi_entity_update_mode}, + ) else: self.sg.update(entity, entity_id, {field_name: new_value}) - new_values = self.sg.find_one(entity, - [['id', 'is', entity_id]], - [field_name]) + new_values = self.sg.find_one(entity, [["id", "is", entity_id]], [field_name]) return new_value, new_values[field_name] class TestUtc(base.LiveTestBase): - '''Test utc options''' + """Test utc options""" def setUp(self): super(TestUtc, self).setUp() @@ -1192,29 +1273,33 @@ def setUp(self): def test_convert_to_utc(self): if self.config.jenkins: self.skipTest("Jenkins. locked_until not updating.") - sg_utc = shotgun_api3.Shotgun(self.config.server_url, - http_proxy=self.config.http_proxy, - convert_datetimes_to_utc=True, - **self.auth_args) + sg_utc = shotgun_api3.Shotgun( + self.config.server_url, + http_proxy=self.config.http_proxy, + convert_datetimes_to_utc=True, + **self.auth_args, + ) self._assert_expected(sg_utc, self.datetime_none, self.datetime_local) self._assert_expected(sg_utc, self.datetime_local, self.datetime_local) def test_no_convert_to_utc(self): if self.config.jenkins: self.skipTest("Jenkins. locked_until not updating.") - sg_no_utc = shotgun_api3.Shotgun(self.config.server_url, - http_proxy=self.config.http_proxy, - convert_datetimes_to_utc=False, - **self.auth_args) + sg_no_utc = shotgun_api3.Shotgun( + self.config.server_url, + http_proxy=self.config.http_proxy, + convert_datetimes_to_utc=False, + **self.auth_args, + ) self._assert_expected(sg_no_utc, self.datetime_none, self.datetime_none) self._assert_expected(sg_no_utc, self.datetime_utc, self.datetime_none) def _assert_expected(self, sg, date_time, expected): - entity_name = 'HumanUser' - entity_id = self.human_user['id'] - field_name = 'locked_until' + entity_name = "HumanUser" + entity_id = self.human_user["id"] + field_name = "locked_until" sg.update(entity_name, entity_id, {field_name: date_time}) - result = sg.find_one(entity_name, [['id', 'is', entity_id]], [field_name]) + result = sg.find_one(entity_name, [["id", "is", entity_id]], [field_name]) self.assertEqual(result[field_name], expected) @@ -1223,31 +1308,33 @@ def setUp(self): super(TestFind, self).setUp() # We will need the created_at field for the shot fields = list(self.shot.keys())[:] - fields.append('created_at') - self.shot = self.sg.find_one('Shot', [['id', 'is', self.shot['id']]], fields) + fields.append("created_at") + self.shot = self.sg.find_one("Shot", [["id", "is", self.shot["id"]]], fields) # We will need the uuid field for our LocalStorage fields = list(self.local_storage.keys())[:] - fields.append('uuid') - self.local_storage = self.sg.find_one('LocalStorage', [['id', 'is', self.local_storage['id']]], fields) + fields.append("uuid") + self.local_storage = self.sg.find_one( + "LocalStorage", [["id", "is", self.local_storage["id"]]], fields + ) def test_find(self): """Called find, find_one for known entities""" filters = [] - filters.append(['project', 'is', self.project]) - filters.append(['id', 'is', self.version['id']]) + filters.append(["project", "is", self.project]) + filters.append(["id", "is", self.version["id"]]) - fields = ['id'] + fields = ["id"] versions = self.sg.find("Version", filters, fields=fields) self.assertTrue(isinstance(versions, list)) version = versions[0] self.assertEqual("Version", version["type"]) - self.assertEqual(self.version['id'], version["id"]) + self.assertEqual(self.version["id"], version["id"]) version = self.sg.find_one("Version", filters, fields=fields) self.assertEqual("Version", version["type"]) - self.assertEqual(self.version['id'], version["id"]) + self.assertEqual(self.version["id"], version["id"]) def _id_in_result(self, entity_type, filters, expected_id): """ @@ -1255,126 +1342,138 @@ def _id_in_result(self, entity_type, filters, expected_id): for particular filters. """ results = self.sg.find(entity_type, filters) - return any(result['id'] == expected_id for result in results) + return any(result["id"] == expected_id for result in results) # TODO test all applicable data types for 'in' - # 'currency' => [BigDecimal, Float, NilClass], - # 'image' => [Hash, NilClass], - # 'percent' => [Bignum, Fixnum, NilClass], - # 'serializable' => [Hash, Array, NilClass], - # 'system_task_type' => [String, NilClass], - # 'timecode' => [Bignum, Fixnum, NilClass], - # 'footage' => [Bignum, Fixnum, NilClass, String, Float, BigDecimal], - # 'url' => [Hash, NilClass], + # 'currency' => [BigDecimal, Float, NilClass], + # 'image' => [Hash, NilClass], + # 'percent' => [Bignum, Fixnum, NilClass], + # 'serializable' => [Hash, Array, NilClass], + # 'system_task_type' => [String, NilClass], + # 'timecode' => [Bignum, Fixnum, NilClass], + # 'footage' => [Bignum, Fixnum, NilClass, String, Float, BigDecimal], + # 'url' => [Hash, NilClass], - # 'uuid' => [String], + # 'uuid' => [String], def test_in_relation_comma_id(self): """ Test that 'in' relation using commas (old format) works with ids. """ - filters = [['id', 'in', self.project['id'], 99999]] - result = self._id_in_result('Project', filters, self.project['id']) + filters = [["id", "in", self.project["id"], 99999]] + result = self._id_in_result("Project", filters, self.project["id"]) self.assertTrue(result) def test_in_relation_list_id(self): """ Test that 'in' relation using list (new format) works with ids. """ - filters = [['id', 'in', [self.project['id'], 99999]]] - result = self._id_in_result('Project', filters, self.project['id']) + filters = [["id", "in", [self.project["id"], 99999]]] + result = self._id_in_result("Project", filters, self.project["id"]) self.assertTrue(result) def test_not_in_relation_id(self): """ Test that 'not_in' relation using commas (old format) works with ids. """ - filters = [['id', 'not_in', self.project['id'], 99999]] - result = self._id_in_result('Project', filters, self.project['id']) + filters = [["id", "not_in", self.project["id"], 99999]] + result = self._id_in_result("Project", filters, self.project["id"]) self.assertFalse(result) def test_in_relation_comma_text(self): """ Test that 'in' relation using commas (old format) works with text fields. """ - filters = [['name', 'in', self.project['name'], 'fake project name']] - result = self._id_in_result('Project', filters, self.project['id']) + filters = [["name", "in", self.project["name"], "fake project name"]] + result = self._id_in_result("Project", filters, self.project["id"]) self.assertTrue(result) def test_in_relation_list_text(self): """ Test that 'in' relation using list (new format) works with text fields. """ - filters = [['name', 'in', [self.project['name'], 'fake project name']]] - result = self._id_in_result('Project', filters, self.project['id']) + filters = [["name", "in", [self.project["name"], "fake project name"]]] + result = self._id_in_result("Project", filters, self.project["id"]) self.assertTrue(result) def test_not_in_relation_text(self): """ Test that 'not_in' relation using commas (old format) works with ids. """ - filters = [['name', 'not_in', [self.project['name'], 'fake project name']]] - result = self._id_in_result('Project', filters, self.project['id']) + filters = [["name", "not_in", [self.project["name"], "fake project name"]]] + result = self._id_in_result("Project", filters, self.project["id"]) self.assertFalse(result) def test_in_relation_comma_color(self): """ Test that 'in' relation using commas (old format) works with color fields. """ - filters = [['color', 'in', self.task['color'], 'Green'], - ['project', 'is', self.project]] + filters = [ + ["color", "in", self.task["color"], "Green"], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_in_relation_list_color(self): """ Test that 'in' relation using list (new format) works with color fields. """ - filters = [['color', 'in', [self.task['color'], 'Green']], - ['project', 'is', self.project]] + filters = [ + ["color", "in", [self.task["color"], "Green"]], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_not_in_relation_color(self): """ Test that 'not_in' relation using commas (old format) works with color fields. """ - filters = [['color', 'not_in', [self.task['color'], 'Green']], - ['project', 'is', self.project]] + filters = [ + ["color", "not_in", [self.task["color"], "Green"]], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertFalse(result) def test_in_relation_comma_date(self): """ Test that 'in' relation using commas (old format) works with date fields. """ - filters = [['due_date', 'in', self.task['due_date'], '2012-11-25'], - ['project', 'is', self.project]] + filters = [ + ["due_date", "in", self.task["due_date"], "2012-11-25"], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_in_relation_list_date(self): """ Test that 'in' relation using list (new format) works with date fields. """ - filters = [['due_date', 'in', [self.task['due_date'], '2012-11-25']], - ['project', 'is', self.project]] + filters = [ + ["due_date", "in", [self.task["due_date"], "2012-11-25"]], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_not_in_relation_date(self): """ Test that 'not_in' relation using commas (old format) works with date fields. """ - filters = [['due_date', 'not_in', [self.task['due_date'], '2012-11-25']], - ['project', 'is', self.project]] + filters = [ + ["due_date", "not_in", [self.task["due_date"], "2012-11-25"]], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertFalse(result) # TODO add datetime test for in and not_in @@ -1385,12 +1484,16 @@ def test_in_relation_comma_duration(self): """ # we need to get the duration value new_task_keys = list(self.task.keys())[:] - new_task_keys.append('duration') - self.task = self.sg.find_one('Task', [['id', 'is', self.task['id']]], new_task_keys) - filters = [['duration', 'in', self.task['duration']], - ['project', 'is', self.project]] + new_task_keys.append("duration") + self.task = self.sg.find_one( + "Task", [["id", "is", self.task["id"]]], new_task_keys + ) + filters = [ + ["duration", "in", self.task["duration"]], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_in_relation_list_duration(self): @@ -1399,12 +1502,22 @@ def test_in_relation_list_duration(self): """ # we need to get the duration value new_task_keys = list(self.task.keys())[:] - new_task_keys.append('duration') - self.task = self.sg.find_one('Task', [['id', 'is', self.task['id']]], new_task_keys) - filters = [['duration', 'in', [self.task['duration'], ]], - ['project', 'is', self.project]] + new_task_keys.append("duration") + self.task = self.sg.find_one( + "Task", [["id", "is", self.task["id"]]], new_task_keys + ) + filters = [ + [ + "duration", + "in", + [ + self.task["duration"], + ], + ], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_not_in_relation_duration(self): @@ -1413,339 +1526,473 @@ def test_not_in_relation_duration(self): """ # we need to get the duration value new_task_keys = list(self.task.keys())[:] - new_task_keys.append('duration') - self.task = self.sg.find_one('Task', [['id', 'is', self.task['id']]], new_task_keys) + new_task_keys.append("duration") + self.task = self.sg.find_one( + "Task", [["id", "is", self.task["id"]]], new_task_keys + ) - filters = [['duration', 'not_in', [self.task['duration'], ]], - ['project', 'is', self.project]] + filters = [ + [ + "duration", + "not_in", + [ + self.task["duration"], + ], + ], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertFalse(result) def test_in_relation_comma_entity(self): """ Test that 'in' relation using commas (old format) works with entity fields. """ - filters = [['entity', 'in', self.task['entity'], self.asset], - ['project', 'is', self.project]] + filters = [ + ["entity", "in", self.task["entity"], self.asset], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_in_relation_list_entity(self): """ Test that 'in' relation using list (new format) works with entity fields. """ - filters = [['entity', 'in', [self.task['entity'], self.asset]], - ['project', 'is', self.project]] + filters = [ + ["entity", "in", [self.task["entity"], self.asset]], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_not_in_relation_entity(self): """ Test that 'not_in' relation using commas (old format) works with entity fields. """ - filters = [['entity', 'not_in', [self.task['entity'], self.asset]], - ['project', 'is', self.project]] + filters = [ + ["entity", "not_in", [self.task["entity"], self.asset]], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertFalse(result) def test_in_relation_comma_entity_type(self): """ Test that 'in' relation using commas (old format) works with entity_type fields. """ - filters = [['entity_type', 'in', self.step['entity_type'], 'something else']] + filters = [["entity_type", "in", self.step["entity_type"], "something else"]] - result = self._id_in_result('Step', filters, self.step['id']) + result = self._id_in_result("Step", filters, self.step["id"]) self.assertTrue(result) def test_in_relation_list_entity_type(self): """ Test that 'in' relation using list (new format) works with entity_type fields. """ - filters = [['entity_type', 'in', [self.step['entity_type'], 'something else']]] + filters = [["entity_type", "in", [self.step["entity_type"], "something else"]]] - result = self._id_in_result('Step', filters, self.step['id']) + result = self._id_in_result("Step", filters, self.step["id"]) self.assertTrue(result) def test_not_in_relation_entity_type(self): """ Test that 'not_in' relation using commas (old format) works with entity_type fields. """ - filters = [['entity_type', 'not_in', [self.step['entity_type'], 'something else']]] + filters = [ + ["entity_type", "not_in", [self.step["entity_type"], "something else"]] + ] - result = self._id_in_result('Step', filters, self.step['id']) + result = self._id_in_result("Step", filters, self.step["id"]) self.assertFalse(result) def test_in_relation_comma_float(self): """ Test that 'in' relation using commas (old format) works with float fields. """ - filters = [['sg_frames_aspect_ratio', 'in', self.version['sg_frames_aspect_ratio'], 44.0], - ['project', 'is', self.project]] + filters = [ + [ + "sg_frames_aspect_ratio", + "in", + self.version["sg_frames_aspect_ratio"], + 44.0, + ], + ["project", "is", self.project], + ] - result = self._id_in_result('Version', filters, self.version['id']) + result = self._id_in_result("Version", filters, self.version["id"]) self.assertTrue(result) def test_in_relation_list_float(self): """ Test that 'in' relation using list (new format) works with float fields. """ - filters = [['sg_frames_aspect_ratio', 'in', [self.version['sg_frames_aspect_ratio'], 30.0]], - ['project', 'is', self.project]] + filters = [ + [ + "sg_frames_aspect_ratio", + "in", + [self.version["sg_frames_aspect_ratio"], 30.0], + ], + ["project", "is", self.project], + ] - result = self._id_in_result('Version', filters, self.version['id']) + result = self._id_in_result("Version", filters, self.version["id"]) self.assertTrue(result) def test_not_in_relation_float(self): """ Test that 'not_in' relation using commas (old format) works with float fields. """ - filters = [['sg_frames_aspect_ratio', 'not_in', [self.version['sg_frames_aspect_ratio'], 4.4]], - ['project', 'is', self.project]] + filters = [ + [ + "sg_frames_aspect_ratio", + "not_in", + [self.version["sg_frames_aspect_ratio"], 4.4], + ], + ["project", "is", self.project], + ] - result = self._id_in_result('Version', filters, self.version['id']) + result = self._id_in_result("Version", filters, self.version["id"]) self.assertFalse(result) def test_in_relation_comma_list(self): """ Test that 'in' relation using commas (old format) works with list fields. """ - filters = [['frame_count', 'in', self.version['frame_count'], 33], - ['project', 'is', self.project]] + filters = [ + ["frame_count", "in", self.version["frame_count"], 33], + ["project", "is", self.project], + ] - result = self._id_in_result('Version', filters, self.version['id']) + result = self._id_in_result("Version", filters, self.version["id"]) self.assertTrue(result) def test_in_relation_list_list(self): """ Test that 'in' relation using list (new format) works with list fields. """ - filters = [['frame_count', 'in', [self.version['frame_count'], 33]], - ['project', 'is', self.project]] + filters = [ + ["frame_count", "in", [self.version["frame_count"], 33]], + ["project", "is", self.project], + ] - result = self._id_in_result('Version', filters, self.version['id']) + result = self._id_in_result("Version", filters, self.version["id"]) self.assertTrue(result) def test_not_in_relation_list(self): """ Test that 'not_in' relation using commas (old format) works with list fields. """ - filters = [['frame_count', 'not_in', [self.version['frame_count'], 33]], - ['project', 'is', self.project]] + filters = [ + ["frame_count", "not_in", [self.version["frame_count"], 33]], + ["project", "is", self.project], + ] - result = self._id_in_result('Version', filters, self.version['id']) + result = self._id_in_result("Version", filters, self.version["id"]) self.assertFalse(result) def test_in_relation_comma_multi_entity(self): """ Test that 'in' relation using commas (old format) works with multi_entity fields. """ - filters = [['task_assignees', 'in', self.human_user, ], - ['project', 'is', self.project]] + filters = [ + [ + "task_assignees", + "in", + self.human_user, + ], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_in_relation_list_multi_entity(self): """ Test that 'in' relation using list (new format) works with multi_entity fields. """ - filters = [['task_assignees', 'in', [self.human_user, ]], - ['project', 'is', self.project]] + filters = [ + [ + "task_assignees", + "in", + [ + self.human_user, + ], + ], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_not_in_relation_multi_entity(self): """ Test that 'not_in' relation using commas (old format) works with multi_entity fields. """ - filters = [['task_assignees', 'not_in', [self.human_user, ]], - ['project', 'is', self.project]] + filters = [ + [ + "task_assignees", + "not_in", + [ + self.human_user, + ], + ], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertFalse(result) def test_in_relation_comma_number(self): """ Test that 'in' relation using commas (old format) works with number fields. """ - filters = [['frame_count', 'in', self.version['frame_count'], 1], - ['project', 'is', self.project]] + filters = [ + ["frame_count", "in", self.version["frame_count"], 1], + ["project", "is", self.project], + ] - result = self._id_in_result('Version', filters, self.version['id']) + result = self._id_in_result("Version", filters, self.version["id"]) self.assertTrue(result) def test_in_relation_list_number(self): """ Test that 'in' relation using list (new format) works with number fields. """ - filters = [['frame_count', 'in', [self.version['frame_count'], 1]], - ['project', 'is', self.project]] + filters = [ + ["frame_count", "in", [self.version["frame_count"], 1]], + ["project", "is", self.project], + ] - result = self._id_in_result('Version', filters, self.version['id']) + result = self._id_in_result("Version", filters, self.version["id"]) self.assertTrue(result) def test_not_in_relation_number(self): """ Test that 'not_in' relation using commas (old format) works with number fields. """ - filters = [['frame_count', 'not_in', [self.version['frame_count'], 1]], - ['project', 'is', self.project]] + filters = [ + ["frame_count", "not_in", [self.version["frame_count"], 1]], + ["project", "is", self.project], + ] - result = self._id_in_result('Version', filters, self.version['id']) + result = self._id_in_result("Version", filters, self.version["id"]) self.assertFalse(result) def test_in_relation_comma_status_list(self): """ Test that 'in' relation using commas (old format) works with status_list fields. """ - filters = [['sg_status_list', 'in', self.task['sg_status_list'], 'fin'], - ['project', 'is', self.project]] + filters = [ + ["sg_status_list", "in", self.task["sg_status_list"], "fin"], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_in_relation_list_status_list(self): """ Test that 'in' relation using list (new format) works with status_list fields. """ - filters = [['sg_status_list', 'in', [self.task['sg_status_list'], 'fin']], - ['project', 'is', self.project]] + filters = [ + ["sg_status_list", "in", [self.task["sg_status_list"], "fin"]], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertTrue(result) def test_not_in_relation_status_list(self): """ Test that 'not_in' relation using commas (old format) works with status_list fields. """ - filters = [['sg_status_list', 'not_in', [self.task['sg_status_list'], 'fin']], - ['project', 'is', self.project]] + filters = [ + ["sg_status_list", "not_in", [self.task["sg_status_list"], "fin"]], + ["project", "is", self.project], + ] - result = self._id_in_result('Task', filters, self.task['id']) + result = self._id_in_result("Task", filters, self.task["id"]) self.assertFalse(result) def test_in_relation_comma_uuid(self): """ Test that 'in' relation using commas (old format) works with uuid fields. """ - filters = [['uuid', 'in', self.local_storage['uuid'], ]] + filters = [ + [ + "uuid", + "in", + self.local_storage["uuid"], + ] + ] - result = self._id_in_result('LocalStorage', filters, self.local_storage['id']) + result = self._id_in_result("LocalStorage", filters, self.local_storage["id"]) self.assertTrue(result) def test_in_relation_list_uuid(self): """ Test that 'in' relation using list (new format) works with uuid fields. """ - filters = [['uuid', 'in', [self.local_storage['uuid'], ]]] + filters = [ + [ + "uuid", + "in", + [ + self.local_storage["uuid"], + ], + ] + ] - result = self._id_in_result('LocalStorage', filters, self.local_storage['id']) + result = self._id_in_result("LocalStorage", filters, self.local_storage["id"]) self.assertTrue(result) def test_not_in_relation_uuid(self): """ Test that 'not_in' relation using commas (old format) works with uuid fields. """ - filters = [['uuid', 'not_in', [self.local_storage['uuid'], ]]] + filters = [ + [ + "uuid", + "not_in", + [ + self.local_storage["uuid"], + ], + ] + ] - result = self._id_in_result('LocalStorage', filters, self.local_storage['id']) + result = self._id_in_result("LocalStorage", filters, self.local_storage["id"]) self.assertFalse(result) def test_find_in(self): """Test use of 'in' relation with find.""" # id # old comma seperated format - filters = [['id', 'in', self.project['id'], 99999]] - projects = self.sg.find('Project', filters) + filters = [["id", "in", self.project["id"], 99999]] + projects = self.sg.find("Project", filters) # can't use 'any' in py 2.4 match = False for project in projects: - if project['id'] == self.project['id']: + if project["id"] == self.project["id"]: match = True self.assertTrue(match) # new list format - filters = [['id', 'in', [self.project['id'], 99999]]] - projects = self.sg.find('Project', filters) + filters = [["id", "in", [self.project["id"], 99999]]] + projects = self.sg.find("Project", filters) # can't use 'any' in py 2.4 match = False for project in projects: - if project['id'] == self.project['id']: + if project["id"] == self.project["id"]: match = True self.assertTrue(match) # text field - filters = [['name', 'in', [self.project['name'], 'fake project name']]] - projects = self.sg.find('Project', filters) + filters = [["name", "in", [self.project["name"], "fake project name"]]] + projects = self.sg.find("Project", filters) project = projects[0] - self.assertEqual(self.project['id'], project['id']) + self.assertEqual(self.project["id"], project["id"]) def test_unsupported_filters(self): - self.assertRaises(shotgun_api3.Fault, self.sg.find_one, 'Shot', - [['image', 'is_not', [{"type": "Thumbnail", "id": 9}]]]) - self.assertRaises(shotgun_api3.Fault, self.sg.find_one, 'HumanUser', [['password_proxy', 'is_not', [None]]]) - self.assertRaises(shotgun_api3.Fault, self.sg.find_one, 'EventLogEntry', [['meta', 'is_not', [None]]]) - self.assertRaises(shotgun_api3.Fault, self.sg.find_one, 'Revision', [['meta', 'attachment', [None]]]) + self.assertRaises( + shotgun_api3.Fault, + self.sg.find_one, + "Shot", + [["image", "is_not", [{"type": "Thumbnail", "id": 9}]]], + ) + self.assertRaises( + shotgun_api3.Fault, + self.sg.find_one, + "HumanUser", + [["password_proxy", "is_not", [None]]], + ) + self.assertRaises( + shotgun_api3.Fault, + self.sg.find_one, + "EventLogEntry", + [["meta", "is_not", [None]]], + ) + self.assertRaises( + shotgun_api3.Fault, + self.sg.find_one, + "Revision", + [["meta", "attachment", [None]]], + ) def test_zero_is_not_none(self): - '''Test the zero and None are differentiated using "is_not" filter. - Ticket #25127 - ''' + """Test the zero and None are differentiated using "is_not" filter. + Ticket #25127 + """ # Create a number field if it doesn't already exist - num_field = 'sg_api_tests_number_field' - if num_field not in list(self.sg.schema_field_read('Asset').keys()): - self.sg.schema_field_create('Asset', 'number', num_field.replace('sg_', '').replace('_', ' ')) + num_field = "sg_api_tests_number_field" + if num_field not in list(self.sg.schema_field_read("Asset").keys()): + self.sg.schema_field_create( + "Asset", "number", num_field.replace("sg_", "").replace("_", " ") + ) # Set to None - self.sg.update('Asset', self.asset['id'], {num_field: None}) + self.sg.update("Asset", self.asset["id"], {num_field: None}) # Should be filtered out - result = self.sg.find('Asset', [['id', 'is', self.asset['id']], [num_field, 'is_not', None]], [num_field]) + result = self.sg.find( + "Asset", + [["id", "is", self.asset["id"]], [num_field, "is_not", None]], + [num_field], + ) self.assertEqual([], result) # Set it to zero - self.sg.update('Asset', self.asset['id'], {num_field: 0}) + self.sg.update("Asset", self.asset["id"], {num_field: 0}) # Should not be filtered out - result = self.sg.find_one('Asset', [['id', 'is', self.asset['id']], [num_field, 'is_not', None]], [num_field]) + result = self.sg.find_one( + "Asset", + [["id", "is", self.asset["id"]], [num_field, "is_not", None]], + [num_field], + ) self.assertFalse(result is None) # Set it to some other number - self.sg.update('Asset', self.asset['id'], {num_field: 1}) + self.sg.update("Asset", self.asset["id"], {num_field: 1}) # Should not be filtered out - result = self.sg.find_one('Asset', [['id', 'is', self.asset['id']], [num_field, 'is_not', None]], [num_field]) + result = self.sg.find_one( + "Asset", + [["id", "is", self.asset["id"]], [num_field, "is_not", None]], + [num_field], + ) self.assertFalse(result is None) def test_include_archived_projects(self): if self.sg.server_caps.version > (5, 3, 13): # Ticket #25082 - result = self.sg.find_one('Shot', [['id', 'is', self.shot['id']]]) - self.assertEqual(self.shot['id'], result['id']) + result = self.sg.find_one("Shot", [["id", "is", self.shot["id"]]]) + self.assertEqual(self.shot["id"], result["id"]) # archive project - self.sg.update('Project', self.project['id'], {'archived': True}) + self.sg.update("Project", self.project["id"], {"archived": True}) # setting defaults to True, so we should get result - result = self.sg.find_one('Shot', [['id', 'is', self.shot['id']]]) - self.assertEqual(self.shot['id'], result['id']) + result = self.sg.find_one("Shot", [["id", "is", self.shot["id"]]]) + self.assertEqual(self.shot["id"], result["id"]) - result = self.sg.find_one('Shot', [['id', 'is', self.shot['id']]], include_archived_projects=False) + result = self.sg.find_one( + "Shot", [["id", "is", self.shot["id"]]], include_archived_projects=False + ) self.assertEqual(None, result) # unarchive project - self.sg.update('Project', self.project['id'], {'archived': False}) + self.sg.update("Project", self.project["id"], {"archived": False}) class TestFollow(base.LiveTestBase): def test_follow_unfollow(self): - '''Test follow method''' + """Test follow method""" if not self.sg.server_caps.version or self.sg.server_caps.version < (5, 1, 22): return @@ -1758,13 +2005,13 @@ def test_follow_unfollow(self): project=self.project, ) as shot: result = self.sg.follow(human_user, shot) - assert(result['followed']) + assert result["followed"] result = self.sg.unfollow(human_user, shot) - assert(result['unfollowed']) + assert result["unfollowed"] def test_followers(self): - '''Test followers method''' + """Test followers method""" if not self.sg.server_caps.version or self.sg.server_caps.version < (5, 1, 22): return @@ -1777,18 +2024,21 @@ def test_followers(self): project=self.project, ) as shot: result = self.sg.follow(human_user, shot) - assert(result['followed']) + assert result["followed"] result = self.sg.followers(shot) self.assertEqual(1, len(result)) - self.assertEqual(human_user['id'], result[0]['id']) + self.assertEqual(human_user["id"], result[0]["id"]) def test_following(self): - '''Test following method''' + """Test following method""" if not self.sg.server_caps.version or self.sg.server_caps.version < (7, 0, 12): - warnings.warn("Test bypassed because PTR server used does not support this feature.", FutureWarning) + warnings.warn( + "Test bypassed because PTR server used does not support this feature.", + FutureWarning, + ) return with self.gen_entity( @@ -1802,14 +2052,14 @@ def test_following(self): project=self.project, ) as task: result = self.sg.follow(human_user, shot) - assert(result['followed']) + assert result["followed"] result = self.sg.following(human_user) self.assertEqual(1, len(result)) result = self.sg.follow(human_user, task) - assert(result['followed']) + assert result["followed"] result = self.sg.following(human_user) @@ -1819,24 +2069,32 @@ def test_following(self): result = self.sg.following(human_user, entity_type="Shot") self.assertEqual(1, len(result)) - shot_project_id = self.sg.find_one("Shot", - [["id", "is", shot["id"]]], - ["project.Project.id"])["project.Project.id"] - task_project_id = self.sg.find_one("Task", - [["id", "is", task["id"]]], - ["project.Project.id"])["project.Project.id"] + shot_project_id = self.sg.find_one( + "Shot", [["id", "is", shot["id"]]], ["project.Project.id"] + )["project.Project.id"] + task_project_id = self.sg.find_one( + "Task", [["id", "is", task["id"]]], ["project.Project.id"] + )["project.Project.id"] project_count = 2 if shot_project_id == task_project_id else 1 - result = self.sg.following(human_user, project={"type": "Project", "id": shot_project_id}) + result = self.sg.following( + human_user, project={"type": "Project", "id": shot_project_id} + ) self.assertEqual(project_count, len(result)) - result = self.sg.following(human_user, project={"type": "Project", "id": task_project_id}) + result = self.sg.following( + human_user, project={"type": "Project", "id": task_project_id} + ) self.assertEqual(project_count, len(result)) - result = self.sg.following(human_user, - project={"type": "Project", "id": shot_project_id}, - entity_type="Shot") + result = self.sg.following( + human_user, + project={"type": "Project", "id": shot_project_id}, + entity_type="Shot", + ) self.assertEqual(1, len(result)) - result = self.sg.following(human_user, - project={"type": "Project", "id": task_project_id}, - entity_type="Task") + result = self.sg.following( + human_user, + project={"type": "Project", "id": task_project_id}, + entity_type="Task", + ) self.assertEqual(1, len(result)) @@ -1846,9 +2104,9 @@ def setUp(self): super(TestErrors, self).setUp(auth_mode) def test_bad_auth(self): - '''test_bad_auth invalid script name or api key raises fault''' + """test_bad_auth invalid script name or api key raises fault""" server_url = self.config.server_url - script_name = 'not_real_script_name' + script_name = "not_real_script_name" api_key = self.config.api_key login = self.config.human_login password = self.config.human_password @@ -1857,48 +2115,94 @@ def test_bad_auth(self): # Test various combinations of illegal arguments self.assertRaises(ValueError, shotgun_api3.Shotgun, server_url) self.assertRaises(ValueError, shotgun_api3.Shotgun, server_url, None, api_key) - self.assertRaises(ValueError, shotgun_api3.Shotgun, server_url, script_name, None) - self.assertRaises(ValueError, shotgun_api3.Shotgun, server_url, script_name, - api_key, login=login, password=password) + self.assertRaises( + ValueError, shotgun_api3.Shotgun, server_url, script_name, None + ) + self.assertRaises( + ValueError, + shotgun_api3.Shotgun, + server_url, + script_name, + api_key, + login=login, + password=password, + ) self.assertRaises(ValueError, shotgun_api3.Shotgun, server_url, login=login) - self.assertRaises(ValueError, shotgun_api3.Shotgun, server_url, password=password) - self.assertRaises(ValueError, shotgun_api3.Shotgun, server_url, script_name, login=login, password=password) - self.assertRaises(ValueError, shotgun_api3.Shotgun, server_url, login=login, auth_token=auth_token) - self.assertRaises(ValueError, shotgun_api3.Shotgun, server_url, password=password, auth_token=auth_token) - self.assertRaises(ValueError, shotgun_api3.Shotgun, server_url, script_name, login=login, - password=password, auth_token=auth_token) - self.assertRaises(ValueError, shotgun_api3.Shotgun, server_url, api_key=api_key, login=login, - password=password, auth_token=auth_token) + self.assertRaises( + ValueError, shotgun_api3.Shotgun, server_url, password=password + ) + self.assertRaises( + ValueError, + shotgun_api3.Shotgun, + server_url, + script_name, + login=login, + password=password, + ) + self.assertRaises( + ValueError, + shotgun_api3.Shotgun, + server_url, + login=login, + auth_token=auth_token, + ) + self.assertRaises( + ValueError, + shotgun_api3.Shotgun, + server_url, + password=password, + auth_token=auth_token, + ) + self.assertRaises( + ValueError, + shotgun_api3.Shotgun, + server_url, + script_name, + login=login, + password=password, + auth_token=auth_token, + ) + self.assertRaises( + ValueError, + shotgun_api3.Shotgun, + server_url, + api_key=api_key, + login=login, + password=password, + auth_token=auth_token, + ) # Test failed authentications sg = shotgun_api3.Shotgun(server_url, script_name, api_key) - self.assertRaises(shotgun_api3.AuthenticationFault, sg.find_one, 'Shot', []) + self.assertRaises(shotgun_api3.AuthenticationFault, sg.find_one, "Shot", []) script_name = self.config.script_name - api_key = 'notrealapikey' + api_key = "notrealapikey" sg = shotgun_api3.Shotgun(server_url, script_name, api_key) - self.assertRaises(shotgun_api3.AuthenticationFault, sg.find_one, 'Shot', []) + self.assertRaises(shotgun_api3.AuthenticationFault, sg.find_one, "Shot", []) - sg = shotgun_api3.Shotgun(server_url, login=login, password='not a real password') - self.assertRaises(shotgun_api3.AuthenticationFault, sg.find_one, 'Shot', []) + sg = shotgun_api3.Shotgun( + server_url, login=login, password="not a real password" + ) + self.assertRaises(shotgun_api3.AuthenticationFault, sg.find_one, "Shot", []) # This may trigger an account lockdown. Make sure it is not locked anymore. user = self.sg.find_one("HumanUser", [["login", "is", login]]) self.sg.update("HumanUser", user["id"], {"locked_until": None}) - @patch('shotgun_api3.shotgun.Http.request') + @patch("shotgun_api3.shotgun.Http.request") def test_status_not_200(self, mock_request): response = MagicMock(name="response mock", spec=dict) response.status = 300 - response.reason = 'reason' + response.reason = "reason" mock_request.return_value = (response, {}) - self.assertRaises(shotgun_api3.ProtocolError, self.sg.find_one, 'Shot', []) + self.assertRaises(shotgun_api3.ProtocolError, self.sg.find_one, "Shot", []) - @patch('shotgun_api3.shotgun.Http.request') + @patch("shotgun_api3.shotgun.Http.request") def test_make_call_retry(self, mock_request): response = MagicMock(name="response mock", spec=dict) response.status = 200 - response.reason = 'reason' + response.reason = "reason" mock_request.return_value = (response, {}) bak_rpc_attempt_interval = self.sg.config.rpc_attempt_interval @@ -1907,15 +2211,13 @@ def test_make_call_retry(self, mock_request): # First: make the request raise a consistent exception mock_request.side_effect = Exception("not working") with self.assertLogs( - 'shotgun_api3', level='DEBUG' - ) as cm1, self.assertRaises( - Exception - ) as cm2: + "shotgun_api3", level="DEBUG" + ) as cm1, self.assertRaises(Exception) as cm2: self.sg.info() self.assertEqual(cm2.exception.args[0], "not working") log_content = "\n".join(cm1.output) - for i in [1,2]: + for i in [1, 2]: self.assertIn( f"Request failed, attempt {i} of 3. Retrying", log_content, @@ -1929,7 +2231,7 @@ def test_make_call_retry(self, mock_request): # retry works def my_side_effect(*args, **kwargs): try: - if my_side_effect.counter<1: + if my_side_effect.counter < 1: raise Exception("not working") return mock.DEFAULT @@ -1938,7 +2240,7 @@ def my_side_effect(*args, **kwargs): my_side_effect.counter = 0 mock_request.side_effect = my_side_effect - with self.assertLogs('shotgun_api3', level='DEBUG') as cm: + with self.assertLogs("shotgun_api3", level="DEBUG") as cm: self.assertIsInstance( self.sg.info(), dict, @@ -1957,7 +2259,7 @@ def my_side_effect(*args, **kwargs): # Last: raise a SSLEOFError exception - SG-34910 def my_side_effect2(*args, **kwargs): try: - if my_side_effect2.counter<1: + if my_side_effect2.counter < 1: raise ssl.SSLEOFError( "EOF occurred in violation of protocol (_ssl.c:2426)" ) @@ -1969,7 +2271,7 @@ def my_side_effect2(*args, **kwargs): my_side_effect2.counter = 0 mock_request.side_effect = my_side_effect2 - with self.assertLogs('shotgun_api3', level='DEBUG') as cm: + with self.assertLogs("shotgun_api3", level="DEBUG") as cm: self.assertIsInstance( self.sg.info(), dict, @@ -1988,7 +2290,7 @@ def my_side_effect2(*args, **kwargs): finally: self.sg.config.rpc_attempt_interval = bak_rpc_attempt_interval - @patch('shotgun_api3.shotgun.Http.request') + @patch("shotgun_api3.shotgun.Http.request") def test_sha2_error(self, mock_request): # Simulate the exception raised with SHA-2 errors mock_request.side_effect = ShotgunSSLError( @@ -2028,7 +2330,7 @@ def test_sha2_error(self, mock_request): if original_env_val is not None: os.environ["SHOTGUN_FORCE_CERTIFICATE_VALIDATION"] = original_env_val - @patch('shotgun_api3.shotgun.Http.request') + @patch("shotgun_api3.shotgun.Http.request") def test_sha2_error_with_strict(self, mock_request): # Simulate the exception raised with SHA-2 errors mock_request.side_effect = ShotgunSSLError( @@ -2059,17 +2361,17 @@ def test_sha2_error_with_strict(self, mock_request): if original_env_val is not None: os.environ["SHOTGUN_FORCE_CERTIFICATE_VALIDATION"] = original_env_val - @patch.object(urllib.request.OpenerDirector, 'open') + @patch.object(urllib.request.OpenerDirector, "open") def test_sanitized_auth_params(self, mock_open): # Simulate the server blowing up and giving us a 500 error - mock_open.side_effect = urllib.error.HTTPError('url', 500, 'message', {}, None) + mock_open.side_effect = urllib.error.HTTPError("url", 500, "message", {}, None) this_dir, _ = os.path.split(__file__) thumbnail_path = os.path.abspath(os.path.join(this_dir, "sg_logo.jpg")) try: # Try to upload a bogus file - self.sg.upload('Note', 1234, thumbnail_path) + self.sg.upload("Note", 1234, thumbnail_path) except shotgun_api3.ShotgunError as e: self.assertFalse(str(self.api_key) in str(e)) return @@ -2084,20 +2386,39 @@ def test_upload_empty_file(self): """ this_dir, _ = os.path.split(__file__) path = os.path.abspath(os.path.expanduser(os.path.join(this_dir, "empty.txt"))) - self.assertRaises(shotgun_api3.ShotgunError, self.sg.upload, 'Version', 123, path) - self.assertRaises(shotgun_api3.ShotgunError, self.sg.upload_thumbnail, 'Version', 123, path) - self.assertRaises(shotgun_api3.ShotgunError, self.sg.upload_filmstrip_thumbnail, 'Version', - 123, path) + self.assertRaises( + shotgun_api3.ShotgunError, self.sg.upload, "Version", 123, path + ) + self.assertRaises( + shotgun_api3.ShotgunError, self.sg.upload_thumbnail, "Version", 123, path + ) + self.assertRaises( + shotgun_api3.ShotgunError, + self.sg.upload_filmstrip_thumbnail, + "Version", + 123, + path, + ) def test_upload_missing_file(self): """ Test uploading an missing file raises an error. """ path = "/path/to/nowhere/foo.txt" - self.assertRaises(shotgun_api3.ShotgunError, self.sg.upload, 'Version', 123, path) - self.assertRaises(shotgun_api3.ShotgunError, self.sg.upload_thumbnail, 'Version', 123, path) - self.assertRaises(shotgun_api3.ShotgunError, self.sg.upload_filmstrip_thumbnail, 'Version', - 123, path) + self.assertRaises( + shotgun_api3.ShotgunError, self.sg.upload, "Version", 123, path + ) + self.assertRaises( + shotgun_api3.ShotgunError, self.sg.upload_thumbnail, "Version", 123, path + ) + self.assertRaises( + shotgun_api3.ShotgunError, + self.sg.upload_filmstrip_thumbnail, + "Version", + 123, + path, + ) + # def test_malformed_response(self): # # TODO ResponseError @@ -2109,9 +2430,9 @@ def setUp(self): super(TestScriptUserSudoAuth, self).setUp() self.sg.update( - 'HumanUser', - self.human_user['id'], - {'projects': [self.project]}, + "HumanUser", + self.human_user["id"], + {"projects": [self.project]}, ) def test_user_is_creator(self): @@ -2122,30 +2443,32 @@ def test_user_is_creator(self): if not self.sg.server_caps.version or self.sg.server_caps.version < (5, 3, 12): return - x = shotgun_api3.Shotgun(self.config.server_url, - http_proxy=self.config.http_proxy, - sudo_as_login=self.config.human_login, - **self.auth_args) + x = shotgun_api3.Shotgun( + self.config.server_url, + http_proxy=self.config.http_proxy, + sudo_as_login=self.config.human_login, + **self.auth_args, + ) data = { - 'project': self.project, - 'code': 'JohnnyApple_Design01_FaceFinal', - 'description': 'fixed rig per director final notes', - 'sg_status_list': 'na', - 'entity': self.asset, - 'user': self.human_user + "project": self.project, + "code": "JohnnyApple_Design01_FaceFinal", + "description": "fixed rig per director final notes", + "sg_status_list": "na", + "entity": self.asset, + "user": self.human_user, } version = x.create("Version", data, return_fields=["id", "created_by"]) self.assertTrue(isinstance(version, dict)) self.assertTrue("id" in version) self.assertTrue("created_by" in version) - self.assertEqual(self.config.human_name, version['created_by']['name']) + self.assertEqual(self.config.human_name, version["created_by"]["name"]) class TestHumanUserSudoAuth(base.TestBase): def setUp(self): - super(TestHumanUserSudoAuth, self).setUp('HumanUser') + super(TestHumanUserSudoAuth, self).setUp("HumanUser") def test_human_user_sudo_auth_fails(self): """ @@ -2158,18 +2481,20 @@ def test_human_user_sudo_auth_fails(self): if not self.sg.server_caps.version or self.sg.server_caps.version < (5, 3, 12): return - x = shotgun_api3.Shotgun(self.config.server_url, - login=self.config.human_login, - password=self.config.human_password, - http_proxy=self.config.http_proxy, - sudo_as_login="blah") - self.assertRaises(shotgun_api3.Fault, x.find_one, 'Shot', []) + x = shotgun_api3.Shotgun( + self.config.server_url, + login=self.config.human_login, + password=self.config.human_password, + http_proxy=self.config.http_proxy, + sudo_as_login="blah", + ) + self.assertRaises(shotgun_api3.Fault, x.find_one, "Shot", []) expected = "The user does not have permission to 'sudo':" try: - x.find_one('Shot', []) + x.find_one("Shot", []) except shotgun_api3.Fault as e: # py24 exceptions don't have message attr - if hasattr(e, 'message'): + if hasattr(e, "message"): self.assertTrue(e.message.startswith(expected)) else: self.assertTrue(e.args[0].startswith(expected)) @@ -2183,30 +2508,31 @@ class TestHumanUserAuth(base.HumanUserAuthLiveTestBase): def test_humanuser_find(self): """Called find, find_one for known entities as human user""" filters = [] - filters.append(['project', 'is', self.project]) - filters.append(['id', 'is', self.version['id']]) + filters.append(["project", "is", self.project]) + filters.append(["id", "is", self.version["id"]]) - fields = ['id'] + fields = ["id"] versions = self.sg.find("Version", filters, fields=fields) self.assertTrue(isinstance(versions, list)) version = versions[0] self.assertEqual("Version", version["type"]) - self.assertEqual(self.version['id'], version["id"]) + self.assertEqual(self.version["id"], version["id"]) version = self.sg.find_one("Version", filters, fields=fields) self.assertEqual("Version", version["type"]) - self.assertEqual(self.version['id'], version["id"]) + self.assertEqual(self.version["id"], version["id"]) def test_humanuser_upload_thumbnail_for_version(self): """simple upload thumbnail for version test as human user.""" this_dir, _ = os.path.split(__file__) - path = os.path.abspath(os.path.expanduser( - os.path.join(this_dir, "sg_logo.jpg"))) + path = os.path.abspath( + os.path.expanduser(os.path.join(this_dir, "sg_logo.jpg")) + ) # upload thumbnail - thumb_id = self.sg.upload_thumbnail("Version", self.version['id'], path) + thumb_id = self.sg.upload_thumbnail("Version", self.version["id"], path) self.assertTrue(isinstance(thumb_id, int)) # check result on version @@ -2215,17 +2541,23 @@ def test_humanuser_upload_thumbnail_for_version(self): [["id", "is", self.version["id"]]], ) - self.assertEqual(version_with_thumbnail.get('type'), 'Version') - self.assertEqual(version_with_thumbnail.get('id'), self.version['id']) + self.assertEqual(version_with_thumbnail.get("type"), "Version") + self.assertEqual(version_with_thumbnail.get("id"), self.version["id"]) h = Http(".cache") - thumb_resp, content = h.request(version_with_thumbnail.get('image'), "GET") - self.assertIn(thumb_resp['status'], ['200', '304']) - self.assertIn(thumb_resp['content-type'], ['image/jpeg', 'image/png']) + thumb_resp, content = h.request(version_with_thumbnail.get("image"), "GET") + self.assertIn(thumb_resp["status"], ["200", "304"]) + self.assertIn(thumb_resp["content-type"], ["image/jpeg", "image/png"]) # clear thumbnail - response_clear_thumbnail = self.sg.update("Version", self.version['id'], {'image': None}) - expected_clear_thumbnail = {'id': self.version['id'], 'image': None, 'type': 'Version'} + response_clear_thumbnail = self.sg.update( + "Version", self.version["id"], {"image": None} + ) + expected_clear_thumbnail = { + "id": self.version["id"], + "image": None, + "type": "Version", + } self.assertEqual(expected_clear_thumbnail, response_clear_thumbnail) @@ -2240,21 +2572,21 @@ def test_humanuser_find(self): if self.sg.server_caps.version >= (5, 4, 1): filters = [] - filters.append(['project', 'is', self.project]) - filters.append(['id', 'is', self.version['id']]) + filters.append(["project", "is", self.project]) + filters.append(["id", "is", self.version["id"]]) - fields = ['id'] + fields = ["id"] versions = self.sg.find("Version", filters, fields=fields) self.assertTrue(isinstance(versions, list)) version = versions[0] self.assertEqual("Version", version["type"]) - self.assertEqual(self.version['id'], version["id"]) + self.assertEqual(self.version["id"], version["id"]) version = self.sg.find_one("Version", filters, fields=fields) self.assertEqual("Version", version["type"]) - self.assertEqual(self.version['id'], version["id"]) + self.assertEqual(self.version["id"], version["id"]) def test_humanuser_upload_thumbnail_for_version(self): """simple upload thumbnail for version test as session based token user.""" @@ -2262,11 +2594,12 @@ def test_humanuser_upload_thumbnail_for_version(self): if self.sg.server_caps.version >= (5, 4, 1): this_dir, _ = os.path.split(__file__) - path = os.path.abspath(os.path.expanduser( - os.path.join(this_dir, "sg_logo.jpg"))) + path = os.path.abspath( + os.path.expanduser(os.path.join(this_dir, "sg_logo.jpg")) + ) # upload thumbnail - thumb_id = self.sg.upload_thumbnail("Version", self.version['id'], path) + thumb_id = self.sg.upload_thumbnail("Version", self.version["id"], path) self.assertTrue(isinstance(thumb_id, int)) # check result on version @@ -2275,17 +2608,23 @@ def test_humanuser_upload_thumbnail_for_version(self): [["id", "is", self.version["id"]]], ) - self.assertEqual(version_with_thumbnail.get('type'), 'Version') - self.assertEqual(version_with_thumbnail.get('id'), self.version['id']) + self.assertEqual(version_with_thumbnail.get("type"), "Version") + self.assertEqual(version_with_thumbnail.get("id"), self.version["id"]) h = Http(".cache") - thumb_resp, content = h.request(version_with_thumbnail.get('image'), "GET") - self.assertIn(thumb_resp['status'], ['200', '304']) - self.assertIn(thumb_resp['content-type'], ['image/jpeg', 'image/png']) + thumb_resp, content = h.request(version_with_thumbnail.get("image"), "GET") + self.assertIn(thumb_resp["status"], ["200", "304"]) + self.assertIn(thumb_resp["content-type"], ["image/jpeg", "image/png"]) # clear thumbnail - response_clear_thumbnail = self.sg.update("Version", self.version['id'], {'image': None}) - expected_clear_thumbnail = {'id': self.version['id'], 'image': None, 'type': 'Version'} + response_clear_thumbnail = self.sg.update( + "Version", self.version["id"], {"image": None} + ) + expected_clear_thumbnail = { + "id": self.version["id"], + "image": None, + "type": "Version", + } self.assertEqual(expected_clear_thumbnail, response_clear_thumbnail) @@ -2295,64 +2634,103 @@ def test_logged_in_user(self): if self.sg.server_caps.version and self.sg.server_caps.version < (5, 3, 20): return - sg = shotgun_api3.Shotgun(self.config.server_url, - login=self.config.human_login, - password=self.config.human_password, - http_proxy=self.config.http_proxy) + sg = shotgun_api3.Shotgun( + self.config.server_url, + login=self.config.human_login, + password=self.config.human_password, + http_proxy=self.config.http_proxy, + ) sg.update_project_last_accessed(self.project) - initial = sg.find_one('Project', [['id', 'is', self.project['id']]], ['last_accessed_by_current_user']) + initial = sg.find_one( + "Project", + [["id", "is", self.project["id"]]], + ["last_accessed_by_current_user"], + ) # Make sure time has elapsed so there is a difference between the two time stamps. time.sleep(2) sg.update_project_last_accessed(self.project) - current = sg.find_one('Project', [['id', 'is', self.project['id']]], ['last_accessed_by_current_user']) + current = sg.find_one( + "Project", + [["id", "is", self.project["id"]]], + ["last_accessed_by_current_user"], + ) self.assertNotEqual(initial, current) # it's possible initial is None - assert(initial['last_accessed_by_current_user'] < current['last_accessed_by_current_user']) + assert ( + initial["last_accessed_by_current_user"] + < current["last_accessed_by_current_user"] + ) def test_pass_in_user(self): if self.sg.server_caps.version and self.sg.server_caps.version < (5, 3, 20): return - sg = shotgun_api3.Shotgun(self.config.server_url, - login=self.config.human_login, - password=self.config.human_password, - http_proxy=self.config.http_proxy) + sg = shotgun_api3.Shotgun( + self.config.server_url, + login=self.config.human_login, + password=self.config.human_password, + http_proxy=self.config.http_proxy, + ) - initial = sg.find_one('Project', [['id', 'is', self.project['id']]], ['last_accessed_by_current_user']) + initial = sg.find_one( + "Project", + [["id", "is", self.project["id"]]], + ["last_accessed_by_current_user"], + ) time.sleep(1) # this instance of the api is not logged in as a user self.sg.update_project_last_accessed(self.project, user=self.human_user) - current = sg.find_one('Project', [['id', 'is', self.project['id']]], ['last_accessed_by_current_user']) + current = sg.find_one( + "Project", + [["id", "is", self.project["id"]]], + ["last_accessed_by_current_user"], + ) self.assertNotEqual(initial, current) # it's possible initial is None if initial: - assert(initial['last_accessed_by_current_user'] < current['last_accessed_by_current_user']) + assert ( + initial["last_accessed_by_current_user"] + < current["last_accessed_by_current_user"] + ) def test_sudo_as_user(self): if self.sg.server_caps.version and self.sg.server_caps.version < (5, 3, 20): return - sg = shotgun_api3.Shotgun(self.config.server_url, - http_proxy=self.config.http_proxy, - sudo_as_login=self.config.human_login, - **self.auth_args) + sg = shotgun_api3.Shotgun( + self.config.server_url, + http_proxy=self.config.http_proxy, + sudo_as_login=self.config.human_login, + **self.auth_args, + ) - initial = sg.find_one('Project', [['id', 'is', self.project['id']]], ['last_accessed_by_current_user']) + initial = sg.find_one( + "Project", + [["id", "is", self.project["id"]]], + ["last_accessed_by_current_user"], + ) time.sleep(1) sg.update_project_last_accessed(self.project) - current = sg.find_one('Project', [['id', 'is', self.project['id']]], ['last_accessed_by_current_user']) + current = sg.find_one( + "Project", + [["id", "is", self.project["id"]]], + ["last_accessed_by_current_user"], + ) self.assertNotEqual(initial, current) # it's possible initial is None if initial: - assert(initial['last_accessed_by_current_user'] < current['last_accessed_by_current_user']) + assert ( + initial["last_accessed_by_current_user"] + < current["last_accessed_by_current_user"] + ) class TestActivityStream(base.LiveTestBase): @@ -2364,36 +2742,51 @@ def setUp(self): super(TestActivityStream, self).setUp() self._prefix = uuid.uuid4().hex - self._shot = self.sg.create("Shot", {"code": "%s activity stream test" % self._prefix, - "project": self.project}) + self._shot = self.sg.create( + "Shot", + {"code": "%s activity stream test" % self._prefix, "project": self.project}, + ) - self._note = self.sg.create("Note", {"content": "Test!", - "project": self.project, - "note_links": [self._shot]}) + self._note = self.sg.create( + "Note", + {"content": "Test!", "project": self.project, "note_links": [self._shot]}, + ) # check that if the created_by is a script user, we want to ensure # that event log generation is enabled for this user. If it has been # disabled, these tests will fail because the activity stream is # connected to events. In this case, print a warning to the user - d = self.sg.find_one("Shot", - [["id", "is", self._shot["id"]]], - ["created_by.ApiUser.generate_event_log_entries"]) + d = self.sg.find_one( + "Shot", + [["id", "is", self._shot["id"]]], + ["created_by.ApiUser.generate_event_log_entries"], + ) if d["created_by.ApiUser.generate_event_log_entries"] is False: # events are turned off! warn the user - print("WARNING! Looks like the script user that is running these " - "tests has got the generate event log entries setting set to " - "off. This will cause the activity stream tests to fail. " - "Please enable event log generation for the script user.") + print( + "WARNING! Looks like the script user that is running these " + "tests has got the generate event log entries setting set to " + "off. This will cause the activity stream tests to fail. " + "Please enable event log generation for the script user." + ) def tearDown(self): batch_data = [] - batch_data.append({"request_type": "delete", - "entity_type": self._note["type"], - "entity_id": self._note["id"]}) - batch_data.append({"request_type": "delete", - "entity_type": self._shot["type"], - "entity_id": self._shot["id"]}) + batch_data.append( + { + "request_type": "delete", + "entity_type": self._note["type"], + "entity_id": self._note["id"], + } + ) + batch_data.append( + { + "request_type": "delete", + "entity_type": self._shot["type"], + "entity_id": self._shot["id"], + } + ) self.sg.batch(batch_data) super(TestActivityStream, self).tearDown() @@ -2406,14 +2799,15 @@ def test_simple(self): if not self.sg.server_caps.version or self.sg.server_caps.version < (6, 2, 0): return - result = self.sg.activity_stream_read(self._shot["type"], - self._shot["id"]) + result = self.sg.activity_stream_read(self._shot["type"], self._shot["id"]) - expected_keys = ["earliest_update_id", - "entity_id", - "entity_type", - "latest_update_id", - "updates"] + expected_keys = [ + "earliest_update_id", + "entity_id", + "entity_type", + "latest_update_id", + "updates", + ] self.assertEqual(set(expected_keys), set(result.keys())) self.assertEqual(len(result["updates"]), 2) @@ -2428,9 +2822,9 @@ def test_limit(self): if not self.sg.server_caps.version or self.sg.server_caps.version < (6, 2, 0): return - result = self.sg.activity_stream_read(self._shot["type"], - self._shot["id"], - limit=1) + result = self.sg.activity_stream_read( + self._shot["type"], self._shot["id"], limit=1 + ) self.assertEqual(len(result["updates"]), 1) self.assertEqual(result["updates"][0]["update_type"], "create") @@ -2444,25 +2838,22 @@ def test_extra_fields(self): if not self.sg.server_caps.version or self.sg.server_caps.version < (6, 2, 0): return - result = self.sg.activity_stream_read(self._shot["type"], - self._shot["id"], - entity_fields={"Shot": ["created_by.HumanUser.image"], - "Note": ["content"]}) + result = self.sg.activity_stream_read( + self._shot["type"], + self._shot["id"], + entity_fields={"Shot": ["created_by.HumanUser.image"], "Note": ["content"]}, + ) self.assertEqual(len(result["updates"]), 2) - self.assertEqual(set(result["updates"][0]["primary_entity"].keys()), - set(["content", - "id", - "name", - "status", - "type"])) + self.assertEqual( + set(result["updates"][0]["primary_entity"].keys()), + set(["content", "id", "name", "status", "type"]), + ) - self.assertEqual(set(result["updates"][1]["primary_entity"].keys()), - set(["created_by.HumanUser.image", - "id", - "name", - "status", - "type"])) + self.assertEqual( + set(result["updates"][1]["primary_entity"].keys()), + set(["created_by.HumanUser.image", "id", "name", "status", "type"]), + ) class TestNoteThreadRead(base.LiveTestBase): @@ -2480,14 +2871,16 @@ def setUp(self): def _check_note(self, data, note_id, additional_fields): # check the expected fields - expected_fields = set(["content", "created_at", "created_by", "id", "type"] + additional_fields) + expected_fields = set( + ["content", "created_at", "created_by", "id", "type"] + additional_fields + ) self.assertEqual(expected_fields, set(data.keys())) # check that the data matches the data we get from a find call - note_data = self.sg.find_one("Note", - [["id", "is", note_id]], - list(expected_fields)) + note_data = self.sg.find_one( + "Note", [["id", "is", note_id]], list(expected_fields) + ) # remove images before comparison if ( "created_by.HumanUser.image" in note_data @@ -2500,13 +2893,15 @@ def _check_note(self, data, note_id, additional_fields): def _check_reply(self, data, reply_id, additional_fields): # check the expected fields - expected_fields = set(["content", "created_at", "user", "id", "type"] + additional_fields) + expected_fields = set( + ["content", "created_at", "user", "id", "type"] + additional_fields + ) self.assertEqual(expected_fields, set(data.keys())) # check that the data matches the data we get from a find call - reply_data = self.sg.find_one("Reply", - [["id", "is", reply_id]], - list(expected_fields)) + reply_data = self.sg.find_one( + "Reply", [["id", "is", reply_id]], list(expected_fields) + ) # the reply stream adds an image to the user fields in order # to include thumbnails for users, so remove this before we compare @@ -2517,13 +2912,15 @@ def _check_reply(self, data, reply_id, additional_fields): def _check_attachment(self, data, attachment_id, additional_fields): # check the expected fields - expected_fields = set(["created_at", "created_by", "id", "type"] + additional_fields) + expected_fields = set( + ["created_at", "created_by", "id", "type"] + additional_fields + ) self.assertEqual(expected_fields, set(data.keys())) # check that the data matches the data we get from a find call - attachment_data = self.sg.find_one("Attachment", - [["id", "is", attachment_id]], - list(expected_fields)) + attachment_data = self.sg.find_one( + "Attachment", [["id", "is", attachment_id]], list(expected_fields) + ) # remove images before comparison if "this_file" in attachment_data and "this_file" in data: @@ -2551,21 +2948,25 @@ def test_simple(self): # reply. For this, make sure that there is a thumbnail # associated with the current user - d = self.sg.find_one("Note", - [["id", "is", note["id"]]], - ["created_by", f"created_by.{user_entity}.image"]) + d = self.sg.find_one( + "Note", + [["id", "is", note["id"]]], + ["created_by", f"created_by.{user_entity}.image"], + ) current_thumbnail = d[f"created_by.{user_entity}.image"] if current_thumbnail is None: # upload thumbnail - self.sg.upload_thumbnail(user_entity, - d["created_by"]["id"], - self._thumbnail_path) + self.sg.upload_thumbnail( + user_entity, d["created_by"]["id"], self._thumbnail_path + ) - d = self.sg.find_one("Note", - [["id", "is", note["id"]]], - ["created_by", f"created_by.{user_entity}.image"]) + d = self.sg.find_one( + "Note", + [["id", "is", note["id"]]], + ["created_by", f"created_by.{user_entity}.image"], + ) current_thumbnail = d[f"created_by.{user_entity}.image"] @@ -2587,8 +2988,10 @@ def test_simple(self): reply_thumb = result[1]["user"]["image"] url_obj_a = urllib.parse.urlparse(current_thumbnail) url_obj_b = urllib.parse.urlparse(reply_thumb) - self.assertEqual("%s/%s" % (url_obj_a.netloc, url_obj_a.path), - "%s/%s" % (url_obj_b.netloc, url_obj_b.path),) + self.assertEqual( + "%s/%s" % (url_obj_a.netloc, url_obj_a.path), + "%s/%s" % (url_obj_b.netloc, url_obj_b.path), + ) # and check ther rest of the data self._check_note(result[0], note["id"], additional_fields=[]) @@ -2615,18 +3018,25 @@ def test_complex(self): return additional_fields = { - "Note": ["created_by.HumanUser.image", - "addressings_to", - "playlist", - "user"], + "Note": [ + "created_by.HumanUser.image", + "addressings_to", + "playlist", + "user", + ], "Reply": ["content"], - "Attachment": ["this_file"] + "Attachment": ["this_file"], } # create note - note = self.sg.create("Note", {"content": "Test!", - "project": self.project, - "addressings_to": [self.human_user]}) + note = self.sg.create( + "Note", + { + "content": "Test!", + "project": self.project, + "addressings_to": [self.human_user], + }, + ) # get thread result = self.sg.note_thread_read(note["id"], additional_fields) @@ -2652,7 +3062,9 @@ def test_complex(self): self._check_note(result[0], note["id"], additional_fields["Note"]) self._check_reply(result[1], reply["id"], additional_fields["Reply"]) - self._check_attachment(result[2], attachment_id, additional_fields["Attachment"]) + self._check_attachment( + result[2], attachment_id, additional_fields["Attachment"] + ) class TestTextSearch(base.LiveTestBase): @@ -2668,14 +3080,16 @@ def setUp(self): batch_data = [] for i in range(5): - data = {"code": "%s Text Search %s" % (self._prefix, i), - "project": self.project} - batch_data.append({"request_type": "create", - "entity_type": "Shot", - "data": data}) - batch_data.append({"request_type": "create", - "entity_type": "Asset", - "data": data}) + data = { + "code": "%s Text Search %s" % (self._prefix, i), + "project": self.project, + } + batch_data.append( + {"request_type": "create", "entity_type": "Shot", "data": data} + ) + batch_data.append( + {"request_type": "create", "entity_type": "Asset", "data": data} + ) data = self.sg.batch(batch_data) self._shot_ids = [x["id"] for x in data if x["type"] == "Shot"] @@ -2686,13 +3100,17 @@ def tearDown(self): # clean up batch_data = [] for shot_id in self._shot_ids: - batch_data.append({"request_type": "delete", - "entity_type": "Shot", - "entity_id": shot_id}) + batch_data.append( + {"request_type": "delete", "entity_type": "Shot", "entity_id": shot_id} + ) for asset_id in self._asset_ids: - batch_data.append({"request_type": "delete", - "entity_type": "Asset", - "entity_id": asset_id}) + batch_data.append( + { + "request_type": "delete", + "entity_type": "Asset", + "entity_id": asset_id, + } + ) self.sg.batch(batch_data) super(TestTextSearch, self).tearDown() @@ -2724,7 +3142,9 @@ def test_limit(self): if not self.sg.server_caps.version or self.sg.server_caps.version < (6, 2, 0): return - result = self.sg.text_search("%s Text Search" % self._prefix, {"Shot": []}, limit=3) + result = self.sg.text_search( + "%s Text Search" % self._prefix, {"Shot": []}, limit=3 + ) matches = result["matches"] self.assertEqual(len(matches), 3) @@ -2735,8 +3155,9 @@ def test_entity_filter(self): if not self.sg.server_caps.version or self.sg.server_caps.version < (6, 2, 0): return - result = self.sg.text_search("%s Text Search" % self._prefix, - {"Shot": [], "Asset": []}) + result = self.sg.text_search( + "%s Text Search" % self._prefix, {"Shot": [], "Asset": []} + ) matches = result["matches"] @@ -2750,12 +3171,15 @@ def test_complex_entity_filter(self): if not self.sg.server_caps.version or self.sg.server_caps.version < (6, 2, 0): return - result = self.sg.text_search("%s Text Search" % self._prefix, - { - "Shot": [["code", "ends_with", "3"]], - "Asset": [{"filter_operator": "any", - "filters": [["code", "ends_with", "4"]]}] - }) + result = self.sg.text_search( + "%s Text Search" % self._prefix, + { + "Shot": [["code", "ends_with", "3"]], + "Asset": [ + {"filter_operator": "any", "filters": [["code", "ends_with", "4"]]} + ], + }, + ) matches = result["matches"] @@ -2775,131 +3199,175 @@ class TestReadAdditionalFilterPresets(base.LiveTestBase): def test_simple_case(self): if self.sg_version < (7, 0, 0): - warnings.warn("Test bypassed because PTR server used does not support this feature.", FutureWarning) + warnings.warn( + "Test bypassed because PTR server used does not support this feature.", + FutureWarning, + ) return - filters = [ - ["project", "is", self.project], - ["id", "is", self.version["id"]] - ] + filters = [["project", "is", self.project], ["id", "is", self.version["id"]]] fields = ["id"] - additional_filters = [{"preset_name": "LATEST", "latest_by": "ENTITIES_CREATED_AT"}] + additional_filters = [ + {"preset_name": "LATEST", "latest_by": "ENTITIES_CREATED_AT"} + ] - versions = self.sg.find("Version", filters, fields=fields, additional_filter_presets=additional_filters) + versions = self.sg.find( + "Version", + filters, + fields=fields, + additional_filter_presets=additional_filters, + ) version = versions[0] self.assertEqual("Version", version["type"]) self.assertEqual(self.version["id"], version["id"]) def test_find_one(self): if self.sg_version < (7, 0, 0): - warnings.warn("Test bypassed because PTR server used does not support this feature.", FutureWarning) + warnings.warn( + "Test bypassed because PTR server used does not support this feature.", + FutureWarning, + ) return - filters = [ - ["project", "is", self.project], - ["id", "is", self.version["id"]] - ] + filters = [["project", "is", self.project], ["id", "is", self.version["id"]]] fields = ["id"] - additional_filters = [{"preset_name": "LATEST", "latest_by": "ENTITIES_CREATED_AT"}] + additional_filters = [ + {"preset_name": "LATEST", "latest_by": "ENTITIES_CREATED_AT"} + ] - version = self.sg.find_one("Version", filters, fields=fields, additional_filter_presets=additional_filters) + version = self.sg.find_one( + "Version", + filters, + fields=fields, + additional_filter_presets=additional_filters, + ) self.assertEqual("Version", version["type"]) self.assertEqual(self.version["id"], version["id"]) def test_filter_with_no_name(self): if self.sg_version < (7, 0, 0): - warnings.warn("Test bypassed because PTR server used does not support this feature.", FutureWarning) + warnings.warn( + "Test bypassed because PTR server used does not support this feature.", + FutureWarning, + ) return - filters = [ - ["project", "is", self.project], - ["id", "is", self.version["id"]] - ] + filters = [["project", "is", self.project], ["id", "is", self.version["id"]]] fields = ["id"] additional_filters = [{}] - self.assertRaises(shotgun_api3.Fault, - self.sg.find, - "Version", filters, fields=fields, additional_filter_presets=additional_filters) + self.assertRaises( + shotgun_api3.Fault, + self.sg.find, + "Version", + filters, + fields=fields, + additional_filter_presets=additional_filters, + ) def test_invalid_filter(self): if self.sg_version < (7, 0, 0): - warnings.warn("Test bypassed because PTR server used does not support this feature.", FutureWarning) + warnings.warn( + "Test bypassed because PTR server used does not support this feature.", + FutureWarning, + ) return - filters = [ - ["project", "is", self.project], - ["id", "is", self.version["id"]] - ] + filters = [["project", "is", self.project], ["id", "is", self.version["id"]]] fields = ["id"] additional_filters = [{"preset_name": "BAD_FILTER"}] - self.assertRaises(shotgun_api3.Fault, - self.sg.find, - "Version", filters, fields=fields, additional_filter_presets=additional_filters) + self.assertRaises( + shotgun_api3.Fault, + self.sg.find, + "Version", + filters, + fields=fields, + additional_filter_presets=additional_filters, + ) def test_filter_not_iterable(self): if self.sg_version < (7, 0, 0): - warnings.warn("Test bypassed because PTR server used does not support this feature.", FutureWarning) + warnings.warn( + "Test bypassed because PTR server used does not support this feature.", + FutureWarning, + ) return - filters = [ - ["project", "is", self.project], - ["id", "is", self.version["id"]] - ] + filters = [["project", "is", self.project], ["id", "is", self.version["id"]]] fields = ["id"] additional_filters = 3 - self.assertRaises(shotgun_api3.Fault, - self.sg.find, - "Version", filters, fields=fields, additional_filter_presets=additional_filters) + self.assertRaises( + shotgun_api3.Fault, + self.sg.find, + "Version", + filters, + fields=fields, + additional_filter_presets=additional_filters, + ) def test_filter_not_list_of_iterable(self): if self.sg_version < (7, 0, 0): - warnings.warn("Test bypassed because PTR server used does not support this feature.", FutureWarning) + warnings.warn( + "Test bypassed because PTR server used does not support this feature.", + FutureWarning, + ) return - filters = [ - ["project", "is", self.project], - ["id", "is", self.version["id"]] - ] + filters = [["project", "is", self.project], ["id", "is", self.version["id"]]] fields = ["id"] additional_filters = [3] - self.assertRaises(shotgun_api3.Fault, - self.sg.find, - "Version", filters, fields=fields, additional_filter_presets=additional_filters) + self.assertRaises( + shotgun_api3.Fault, + self.sg.find, + "Version", + filters, + fields=fields, + additional_filter_presets=additional_filters, + ) def test_multiple_latest_filters(self): if self.sg_version < (7, 0, 0): - warnings.warn("Test bypassed because PTR server used does not support this feature.", FutureWarning) + warnings.warn( + "Test bypassed because PTR server used does not support this feature.", + FutureWarning, + ) return - filters = [ - ["project", "is", self.project], - ["id", "is", self.version["id"]] - ] + filters = [["project", "is", self.project], ["id", "is", self.version["id"]]] fields = ["id"] - additional_filters = ({"preset_name": "LATEST", "latest_by": "ENTITY_CREATED_AT"}, - {"preset_name": "LATEST", "latest_by": "PIPELINE_STEP_NUMBER_AND_ENTITIES_CREATED_AT"}) + additional_filters = ( + {"preset_name": "LATEST", "latest_by": "ENTITY_CREATED_AT"}, + { + "preset_name": "LATEST", + "latest_by": "PIPELINE_STEP_NUMBER_AND_ENTITIES_CREATED_AT", + }, + ) - self.assertRaises(shotgun_api3.Fault, - self.sg.find, - "Version", filters, fields=fields, additional_filter_presets=additional_filters) + self.assertRaises( + shotgun_api3.Fault, + self.sg.find, + "Version", + filters, + fields=fields, + additional_filter_presets=additional_filters, + ) def test_modify_visibility(self): """ @@ -2908,7 +3376,10 @@ def test_modify_visibility(self): # If the version of Shotgun is too old, do not run this test. # TODO: Update this with the real version number once the feature is released. if self.sg_version < (8, 5, 0): - warnings.warn("Test bypassed because PTR server used does not support this feature.", FutureWarning) + warnings.warn( + "Test bypassed because PTR server used does not support this feature.", + FutureWarning, + ) return field_display_name = "Project Visibility Test" @@ -2920,7 +3391,9 @@ def test_modify_visibility(self): self.sg.schema_field_create("Asset", "text", "Project Visibility Test") # Grab any two projects that we can use for toggling the visible property with. - projects = self.sg.find("Project", [], order=[{"field_name": "id", "direction": "asc"}]) + projects = self.sg.find( + "Project", [], order=[{"field_name": "id", "direction": "asc"}] + ) project_1 = projects[0] project_2 = projects[1] @@ -2929,21 +3402,27 @@ def test_modify_visibility(self): self.sg.schema_field_update("Asset", field_name, {"visible": True}, project_1) self.assertEqual( {"value": True, "editable": True}, - self.sg.schema_field_read("Asset", field_name, project_1)[field_name]["visible"] + self.sg.schema_field_read("Asset", field_name, project_1)[field_name][ + "visible" + ], ) self.sg.schema_field_update("Asset", field_name, {"visible": True}, project_2) self.assertEqual( {"value": True, "editable": True}, - self.sg.schema_field_read("Asset", field_name, project_2)[field_name]["visible"] + self.sg.schema_field_read("Asset", field_name, project_2)[field_name][ + "visible" + ], ) # Built-in fields should remain not editable. - self.assertFalse(self.sg.schema_field_read("Asset", "code")["code"]["visible"]["editable"]) + self.assertFalse( + self.sg.schema_field_read("Asset", "code")["code"]["visible"]["editable"] + ) # Custom fields should be editable self.assertEqual( {"value": True, "editable": True}, - self.sg.schema_field_read("Asset", field_name)[field_name]["visible"] + self.sg.schema_field_read("Asset", field_name)[field_name]["visible"], ) # Hide the field on project 1 @@ -2951,20 +3430,26 @@ def test_modify_visibility(self): # It should not be visible anymore. self.assertEqual( {"value": False, "editable": True}, - self.sg.schema_field_read("Asset", field_name, project_1)[field_name]["visible"] + self.sg.schema_field_read("Asset", field_name, project_1)[field_name][ + "visible" + ], ) # The field should be visible on the second project. self.assertEqual( {"value": True, "editable": True}, - self.sg.schema_field_read("Asset", field_name, project_2)[field_name]["visible"] + self.sg.schema_field_read("Asset", field_name, project_2)[field_name][ + "visible" + ], ) # Restore the visibility on the field. self.sg.schema_field_update("Asset", field_name, {"visible": True}, project_1) self.assertEqual( {"value": True, "editable": True}, - self.sg.schema_field_read("Asset", field_name, project_1)[field_name]["visible"] + self.sg.schema_field_read("Asset", field_name, project_1)[field_name][ + "visible" + ], ) @@ -2983,6 +3468,7 @@ def test_import_httplib(self): proxied to allow this. """ from shotgun_api3.lib import httplib2 + # Ensure that Http object is available. This is a good indication that # the httplib2 module contents are importable. self.assertTrue(hasattr(httplib2, "Http")) @@ -3003,6 +3489,7 @@ def test_import_httplib(self): # import -- this is a good indication that external httplib2 imports # from shotgun_api3 will work as expected. from shotgun_api3.lib.httplib2 import socks + self.assertTrue(isinstance(socks, types.ModuleType)) # Make sure that objects in socks are available as expected self.assertTrue(hasattr(socks, "HTTPError")) @@ -3025,7 +3512,7 @@ def _get_path(url): """ # url_parse returns native objects for older python versions (2.4) if isinstance(url, dict): - return url.get('path') + return url.get("path") elif isinstance(url, tuple): # 3rd component is the path return url[2] @@ -3033,5 +3520,5 @@ def _get_path(url): return url.path -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_client.py b/tests/test_client.py index dc3fa3ec5..e29c6158d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -18,6 +18,7 @@ from shotgun_api3.lib.six.moves import urllib from shotgun_api3.lib import six, sgutils + try: import simplejson as json except ImportError: @@ -48,12 +49,12 @@ def b64encode(val): class TestShotgunClient(base.MockTestBase): - '''Test case for shotgun api with server interactions mocked.''' + """Test case for shotgun api with server interactions mocked.""" def setUp(self): super(TestShotgunClient, self).setUp() # get domain and uri scheme - match = re.search('(https?://)(.*)', self.server_url) + match = re.search("(https?://)(.*)", self.server_url) self.uri_prefix = match.group(1) self.domain = match.group(2) # always want the mock on @@ -75,8 +76,8 @@ def test_detect_client_caps(self): # todo test for version string (eg. "1.2.3ng") or "unknown" def test_detect_server_caps(self): - '''test_detect_server_caps tests that ServerCapabilities object is made - with appropriate settings for given server version.''' + """test_detect_server_caps tests that ServerCapabilities object is made + with appropriate settings for given server version.""" # has paging is tested else where. server_info = {"version": [9, 9, 9]} self._mock_http(server_info) @@ -94,12 +95,14 @@ def test_detect_server_caps(self): self.assertTrue(self.sg.server_caps.is_dev) def test_server_version_json(self): - '''test_server_version_json tests expected versions for json support.''' + """test_server_version_json tests expected versions for json support.""" sc = ServerCapabilities("foo", {"version": (2, 4, 0)}) sc.version = (2, 3, 99) self.assertRaises(api.ShotgunError, sc._ensure_json_supported) - self.assertRaises(api.ShotgunError, ServerCapabilities, "foo", {"version": (2, 2, 0)}) + self.assertRaises( + api.ShotgunError, ServerCapabilities, "foo", {"version": (2, 2, 0)} + ) sc.version = (0, 0, 0) self.assertRaises(api.ShotgunError, sc._ensure_json_supported) @@ -146,18 +149,20 @@ def auth_args(): self.assertRaises(api.Fault, self.sg.delete, "FakeType", 1) self.assertTrue("session_uuid" not in auth_args()) - my_uuid = '5a1d49b0-0c69-11e0-a24c-003048d17544' + my_uuid = "5a1d49b0-0c69-11e0-a24c-003048d17544" self.sg.set_session_uuid(my_uuid) self.assertRaises(api.Fault, self.sg.delete, "FakeType", 1) self.assertEqual(my_uuid, auth_args()["session_uuid"]) def test_url(self): """Server url is parsed correctly""" - login = self.human_user['login'] + login = self.human_user["login"] password = self.human_password self.assertRaises(ValueError, api.Shotgun, None, None, None, connect=False) - self.assertRaises(ValueError, api.Shotgun, "file://foo.com", None, None, connect=False) + self.assertRaises( + ValueError, api.Shotgun, "file://foo.com", None, None, connect=False + ) self.assertEqual("/api3/json", self.sg.config.api_path) @@ -174,7 +179,7 @@ def test_b64encode(self): login = "thelogin" password = "%thepassw0r#$" login_password = "%s:%s" % (login, password) - expected = 'dGhlbG9naW46JXRoZXBhc3N3MHIjJA==' + expected = "dGhlbG9naW46JXRoZXBhc3N3MHIjJA==" result = b64encode(urllib.parse.unquote(login_password)).strip() self.assertEqual(expected, result) @@ -192,8 +197,7 @@ def test_read_config(self): def test_split_url(self): """Validate that url parts are properly extracted.""" - sg = api.Shotgun("https://ci.shotgunstudio.com", - "foo", "bar", connect=False) + sg = api.Shotgun("https://ci.shotgunstudio.com", "foo", "bar", connect=False) base_url = "https://ci.shotgunstudio.com" expected_server = "ci.shotgunstudio.com" @@ -225,7 +229,7 @@ def test_split_url(self): def test_authorization(self): """Authorization passed to server""" - login = self.human_user['login'] + login = self.human_user["login"] password = self.human_password login_password = "%s:%s" % (login, password) # login:password@domain @@ -233,7 +237,7 @@ def test_authorization(self): self.sg = api.Shotgun(auth_url, "foo", "bar", connect=False) self._setup_mock() - self._mock_http({'version': [2, 4, 0, u'Dev']}) + self._mock_http({"version": [2, 4, 0, "Dev"]}) self.sg.info() @@ -279,7 +283,7 @@ def test_user_agent(self): client_caps.py_version, client_caps.platform.capitalize(), client_caps.ssl_version, - ssl_validate_lut[config.no_ssl_validation] + ssl_validate_lut[config.no_ssl_validation], ) self.assertEqual(expected, headers.get("user-agent")) @@ -293,7 +297,7 @@ def test_user_agent(self): client_caps.py_version, client_caps.platform.capitalize(), client_caps.ssl_version, - ssl_validate_lut[config.no_ssl_validation] + ssl_validate_lut[config.no_ssl_validation], ) self.assertEqual(expected, headers.get("user-agent")) @@ -307,7 +311,7 @@ def test_user_agent(self): client_caps.py_version, client_caps.platform.capitalize(), client_caps.ssl_version, - ssl_validate_lut[config.no_ssl_validation] + ssl_validate_lut[config.no_ssl_validation], ) self.assertEqual(expected, headers.get("user-agent")) @@ -327,14 +331,15 @@ def test_network_retry(self): self.assertRaises(httplib2.HttpLib2Error, self.sg.info) self.assertTrue( self.sg.config.max_rpc_attempts == self.sg._http_request.call_count, - "Call is repeated") + "Call is repeated", + ) # Ensure that sleep was called with the retry interval between each attempt attempt_interval = self.sg.config.rpc_attempt_interval / 1000.0 calls = [mock.callargs(((attempt_interval,), {}))] - calls *= (self.sg.config.max_rpc_attempts - 1) + calls *= self.sg.config.max_rpc_attempts - 1 self.assertTrue( mock_sleep.call_args_list == calls, - "Call is repeated at correct interval." + "Call is repeated at correct interval.", ) def test_set_retry_interval(self): @@ -342,12 +347,15 @@ def test_set_retry_interval(self): original_env_val = os.environ.pop("SHOTGUN_API_RETRY_INTERVAL", None) try: + def run_interval_test(expected_interval, interval_property=None): - self.sg = api.Shotgun(self.config.server_url, - self.config.script_name, - self.config.api_key, - http_proxy=self.config.http_proxy, - connect=self.connect) + self.sg = api.Shotgun( + self.config.server_url, + self.config.script_name, + self.config.api_key, + http_proxy=self.config.http_proxy, + connect=self.connect, + ) self._setup_mock() if interval_property: # if a value was provided for interval_property, set the @@ -424,7 +432,10 @@ def test_call_rpc(self): # Test unicode mixed with utf-8 as reported in Ticket #17959 d = {"results": ["foo", "bar"]} - a = {"utf_str": "\xe2\x88\x9a", "unicode_str": sgutils.ensure_text("\xe2\x88\x9a")} + a = { + "utf_str": "\xe2\x88\x9a", + "unicode_str": sgutils.ensure_text("\xe2\x88\x9a"), + } self._mock_http(d) rv = self.sg._call_rpc("list", a) expected = "rpc response with list result" @@ -460,11 +471,14 @@ def test_upload_s3_503(self): """ this_dir, _ = os.path.split(__file__) storage_url = "http://foo.com/" - path = os.path.abspath(os.path.expanduser( - os.path.join(this_dir, "sg_logo.jpg"))) + path = os.path.abspath( + os.path.expanduser(os.path.join(this_dir, "sg_logo.jpg")) + ) # Expected HTTPError exception error message - expected = "The server is currently down or to busy to reply." \ - "Please try again later." + expected = ( + "The server is currently down or to busy to reply." + "Please try again later." + ) # Test the Internal function that is used to upload each # data part in the context of multi-part uploads to S3, we @@ -474,8 +488,9 @@ def test_upload_s3_503(self): # Test the max retries attempt self.assertTrue( self.sg.MAX_ATTEMPTS == self.sg._make_upload_request.call_count, - f"Call is repeated up to {self.sg.MAX_ATTEMPTS} times") - + f"Call is repeated up to {self.sg.MAX_ATTEMPTS} times", + ) + def test_upload_s3_500(self): """ Test 500 response is retried when uploading to S3. @@ -483,11 +498,14 @@ def test_upload_s3_500(self): self._setup_mock(s3_status_code_error=500) this_dir, _ = os.path.split(__file__) storage_url = "http://foo.com/" - path = os.path.abspath(os.path.expanduser( - os.path.join(this_dir, "sg_logo.jpg"))) + path = os.path.abspath( + os.path.expanduser(os.path.join(this_dir, "sg_logo.jpg")) + ) # Expected HTTPError exception error message - expected = "The server is currently down or to busy to reply." \ - "Please try again later." + expected = ( + "The server is currently down or to busy to reply." + "Please try again later." + ) # Test the Internal function that is used to upload each # data part in the context of multi-part uploads to S3, we @@ -497,8 +515,9 @@ def test_upload_s3_500(self): # Test the max retries attempt self.assertTrue( self.sg.MAX_ATTEMPTS == self.sg._make_upload_request.call_count, - f"Call is repeated up to {self.sg.MAX_ATTEMPTS} times") - + f"Call is repeated up to {self.sg.MAX_ATTEMPTS} times", + ) + def test_upload_s3_urlerror__get_attachment_upload_info(self): """ Test URLError response is retried when invoking _send_form @@ -520,7 +539,7 @@ def test_upload_s3_urlerror__get_attachment_upload_info(self): self.assertEqual( self.sg.MAX_ATTEMPTS, mock_opener.return_value.open.call_count, - f"Call is repeated up to {self.sg.MAX_ATTEMPTS} times" + f"Call is repeated up to {self.sg.MAX_ATTEMPTS} times", ) # Test the exception message @@ -554,7 +573,7 @@ def test_upload_s3_urlerror__upload_to_storage(self): self.assertEqual( self.sg.MAX_ATTEMPTS, self.sg._make_upload_request.call_count, - f"Call is repeated up to {self.sg.MAX_ATTEMPTS} times" + f"Call is repeated up to {self.sg.MAX_ATTEMPTS} times", ) # Test the exception message @@ -566,19 +585,15 @@ def test_transform_data(self): timestamp = time.time() # microseconds will be last during transforms now = datetime.datetime.fromtimestamp(timestamp).replace( - microsecond=0, tzinfo=SG_TIMEZONE.local) - utc_now = datetime.datetime.utcfromtimestamp(timestamp).replace( - microsecond=0) - local = { - "date": now.strftime('%Y-%m-%d'), - "datetime": now, - "time": now.time() - } + microsecond=0, tzinfo=SG_TIMEZONE.local + ) + utc_now = datetime.datetime.utcfromtimestamp(timestamp).replace(microsecond=0) + local = {"date": now.strftime("%Y-%m-%d"), "datetime": now, "time": now.time()} # date will still be the local date, because they are not transformed utc = { - "date": now.strftime('%Y-%m-%d'), + "date": now.strftime("%Y-%m-%d"), "datetime": utc_now, - "time": utc_now.time() + "time": utc_now.time(), } def _datetime(s, f): @@ -587,7 +602,7 @@ def _datetime(s, f): def assert_wire(wire, match): self.assertTrue(isinstance(wire["date"], str)) d = _datetime(wire["date"], "%Y-%m-%d").date() - d = wire['date'] + d = wire["date"] self.assertEqual(match["date"], d) self.assertTrue(isinstance(wire["datetime"], str)) d = _datetime(wire["datetime"], "%Y-%m-%dT%H:%M:%SZ") @@ -619,33 +634,35 @@ def assert_wire(wire, match): def test_encode_payload(self): """Request body is encoded as JSON""" - d = {"this is ": u"my data \u00E0"} + d = {"this is ": "my data \u00e0"} j = self.sg._encode_payload(d) self.assertTrue(isinstance(j, bytes)) - d = { - "this is ": u"my data" - } + d = {"this is ": "my data"} j = self.sg._encode_payload(d) self.assertTrue(isinstance(j, bytes)) def test_decode_response_ascii(self): - self._assert_decode_resonse(True, sgutils.ensure_str(u"my data \u00E0", encoding='utf8')) + self._assert_decode_resonse( + True, sgutils.ensure_str("my data \u00e0", encoding="utf8") + ) def test_decode_response_unicode(self): - self._assert_decode_resonse(False, u"my data \u00E0") + self._assert_decode_resonse(False, "my data \u00e0") def _assert_decode_resonse(self, ensure_ascii, data): """HTTP Response is decoded as JSON or text""" headers = {"content-type": "application/json;charset=utf-8"} d = {"this is ": data} - sg = api.Shotgun(self.config.server_url, - self.config.script_name, - self.config.api_key, - http_proxy=self.config.http_proxy, - ensure_ascii=ensure_ascii, - connect=False) + sg = api.Shotgun( + self.config.server_url, + self.config.script_name, + self.config.api_key, + http_proxy=self.config.http_proxy, + ensure_ascii=ensure_ascii, + connect=False, + ) if six.PY3: j = json.dumps(d, ensure_ascii=ensure_ascii) @@ -663,11 +680,11 @@ def test_parse_records(self): """Parse records to replace thumbnail and local paths""" system = platform.system().lower() - if system == 'darwin': + if system == "darwin": local_path_field = "local_path_mac" - elif system in ['windows', 'microsoft']: + elif system in ["windows", "microsoft"]: local_path_field = "local_path_windows" - elif system == 'linux': + elif system == "linux": local_path_field = "local_path_linux" orig = { "type": "FakeAsset", @@ -676,11 +693,10 @@ def test_parse_records(self): "foo": { "link_type": "local", local_path_field: "/foo/bar.jpg", - } + }, } url = "http://foo/files/0000/0000/0012/232/shot_thumb.jpg" - self.sg._build_thumb_url = mock.Mock( - return_value=url) + self.sg._build_thumb_url = mock.Mock(return_value=url) modified, txt = self.sg._parse_records([orig, "plain text"]) self.assertEqual("plain text", txt, "non dict value is left as is") @@ -703,14 +719,15 @@ def test_thumb_url(self): url = self.sg._build_thumb_url("FakeAsset", 1234) - self.assertEqual( - "http://foo.com/files/0000/0000/0012/232/shot_thumb.jpg", url) + self.assertEqual("http://foo.com/files/0000/0000/0012/232/shot_thumb.jpg", url) self.assertTrue(self.sg._http_request.called, "http request made to get url") args, _ = self.sg._http_request.call_args verb, path, body, headers = args self.assertEqual( "/upload/get_thumbnail_url?entity_type=FakeAsset&entity_id=1234", - path, "thumbnail url called with correct args") + path, + "thumbnail url called with correct args", + ) resp = "0\nSome Error" self._mock_http(resp, headers={"content-type": "text/plain"}) @@ -722,27 +739,34 @@ def test_thumb_url(self): class TestShotgunClientInterface(base.MockTestBase): - '''Tests expected interface for shotgun module and client''' + """Tests expected interface for shotgun module and client""" def test_client_interface(self): - expected_attributes = ['base_url', - 'config', - 'client_caps', - 'server_caps'] + expected_attributes = ["base_url", "config", "client_caps", "server_caps"] for expected_attribute in expected_attributes: if not hasattr(self.sg, expected_attribute): - assert False, '%s not found on %s' % (expected_attribute, - self.sg) + assert False, "%s not found on %s" % (expected_attribute, self.sg) def test_module_interface(self): import shotgun_api3 - expected_contents = ['Shotgun', 'ShotgunError', 'Fault', - 'ProtocolError', 'ResponseError', 'Error', - 'sg_timezone', '__version__'] + + expected_contents = [ + "Shotgun", + "ShotgunError", + "Fault", + "ProtocolError", + "ResponseError", + "Error", + "sg_timezone", + "__version__", + ] for expected_content in expected_contents: if not hasattr(shotgun_api3, expected_content): - assert False, '%s not found on module %s' % (expected_content, shotgun_api3) + assert False, "%s not found on module %s" % ( + expected_content, + shotgun_api3, + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_config_file b/tests/test_config_file index 8215eecde..d642f96c2 100644 --- a/tests/test_config_file +++ b/tests/test_config_file @@ -4,4 +4,4 @@ script_name : xyz api_key : %%abce [TEST_DATA] -project_name : hjkl \ No newline at end of file +project_name : hjkl diff --git a/tests/test_mockgun.py b/tests/test_mockgun.py index 84e5cb2e7..1395355fa 100644 --- a/tests/test_mockgun.py +++ b/tests/test_mockgun.py @@ -42,14 +42,11 @@ from shotgun_api3 import ShotgunError -mockgun_schema_folder = os.path.join( - os.path.dirname(__file__), - "mockgun" -) +mockgun_schema_folder = os.path.join(os.path.dirname(__file__), "mockgun") Mockgun.set_schema_paths( os.path.join(mockgun_schema_folder, "schema.pickle"), - os.path.join(mockgun_schema_folder, "schema_entity.pickle") + os.path.join(mockgun_schema_folder, "schema_entity.pickle"), ) @@ -64,6 +61,7 @@ def test_interface_intact(self): """ from shotgun_api3.lib import mockgun + # Try to access everything. If something is missing, it will raise an # error. mockgun.MockgunError @@ -82,7 +80,9 @@ def setUp(self): """ super(TestValidateFilterSyntax, self).setUp() - self._mockgun = Mockgun("https://test.shotgunstudio.com", login="user", password="1234") + self._mockgun = Mockgun( + "https://test.shotgunstudio.com", login="user", password="1234" + ) self._mockgun.create("Shot", {"code": "shot"}) @@ -94,24 +94,16 @@ def test_filter_array_or_dict(self): self._mockgun.find( "Shot", [ - { - "filter_operator": "any", - "filters": [["code", "is", "shot"]] - }, - [ - "code", "is", "shot" - ] - ] + {"filter_operator": "any", "filters": [["code", "is", "shot"]]}, + ["code", "is", "shot"], + ], ) # We can't have not dict/list values for filters however. self.assertRaisesRegex( ShotgunError, "Filters can only be lists or dictionaries, not int.", - lambda: self._mockgun.find( - "Shot", - [1] - ) + lambda: self._mockgun.find("Shot", [1]), ) @@ -124,14 +116,21 @@ def setUp(self): """ Creates test data. """ - self._mockgun = Mockgun("https://test.shotgunstudio.com", login="user", password="1234") + self._mockgun = Mockgun( + "https://test.shotgunstudio.com", login="user", password="1234" + ) - self._project_link = self._mockgun.create("Project", {"name": "project", "archived": False}) + self._project_link = self._mockgun.create( + "Project", {"name": "project", "archived": False} + ) # This entity will ensure that a populated link field will be comparable. self._mockgun.create( "PipelineConfiguration", - {"code": "with_project", "project": self._project_link, } + { + "code": "with_project", + "project": self._project_link, + }, ) # This entity will ensure that an unpopulated link field will be comparable. @@ -145,17 +144,23 @@ def test_searching_for_none_entity_field(self): items = self._mockgun.find("PipelineConfiguration", [["project", "is", None]]) self.assertEqual(len(items), 1) - items = self._mockgun.find("PipelineConfiguration", [["project", "is_not", None]]) + items = self._mockgun.find( + "PipelineConfiguration", [["project", "is_not", None]] + ) self.assertEqual(len(items), 1) def test_searching_for_initialized_entity_field(self): """ Ensures that comparison with an entity works. """ - items = self._mockgun.find("PipelineConfiguration", [["project", "is", self._project_link]]) + items = self._mockgun.find( + "PipelineConfiguration", [["project", "is", self._project_link]] + ) self.assertEqual(len(items), 1) - items = self._mockgun.find("PipelineConfiguration", [["project", "is_not", self._project_link]]) + items = self._mockgun.find( + "PipelineConfiguration", [["project", "is_not", self._project_link]] + ) self.assertEqual(len(items), 1) def test_find_entity_with_none_link(self): @@ -164,7 +169,9 @@ def test_find_entity_with_none_link(self): """ # The pipeline configuration without_project doesn't have the project field set, so we're expecting # it to not be returned here. - items = self._mockgun.find("PipelineConfiguration", [["project.Project.archived", "is", False]]) + items = self._mockgun.find( + "PipelineConfiguration", [["project.Project.archived", "is", False]] + ) self.assertEqual(len(items), 1) self.assertEqual(items[0]["id"], self._project_link["id"]) @@ -173,11 +180,14 @@ class TestTextFieldOperators(unittest.TestCase): """ Checks if text field comparison work. """ + def setUp(self): """ Creates test data. """ - self._mockgun = Mockgun("https://test.shotgunstudio.com", login="user", password="1234") + self._mockgun = Mockgun( + "https://test.shotgunstudio.com", login="user", password="1234" + ) self._user = self._mockgun.create("HumanUser", {"login": "user"}) def test_operator_contains(self): @@ -198,7 +208,9 @@ def setUp(self): Creates test data. """ - self._mockgun = Mockgun("https://test.shotgunstudio.com", login="user", password="1234") + self._mockgun = Mockgun( + "https://test.shotgunstudio.com", login="user", password="1234" + ) # Create two users to assign to the pipeline configurations. self._user1 = self._mockgun.create("HumanUser", {"login": "user1"}) @@ -206,54 +218,67 @@ def setUp(self): # Create pipeline configurations that are assigned none, one or two users. self._mockgun.create( - "PipelineConfiguration", - {"code": "with_user1", "users": [self._user1]} + "PipelineConfiguration", {"code": "with_user1", "users": [self._user1]} ) self._mockgun.create( - "PipelineConfiguration", - {"code": "with_user2", "users": [self._user2]} + "PipelineConfiguration", {"code": "with_user2", "users": [self._user2]} ) self._mockgun.create( "PipelineConfiguration", - {"code": "with_both", "users": [self._user2, self._user1]} + {"code": "with_both", "users": [self._user2, self._user1]}, ) self._mockgun.create( - "PipelineConfiguration", - {"code": "with_none", "users": []} + "PipelineConfiguration", {"code": "with_none", "users": []} ) def test_find_by_sub_entity_field(self): """ Ensures that queries on linked entity fields works. """ - items = self._mockgun.find("PipelineConfiguration", [["users.HumanUser.login", "is", "user1"]]) + items = self._mockgun.find( + "PipelineConfiguration", [["users.HumanUser.login", "is", "user1"]] + ) self.assertEqual(len(items), 2) - items = self._mockgun.find("PipelineConfiguration", [["users.HumanUser.login", "is", "user2"]]) + items = self._mockgun.find( + "PipelineConfiguration", [["users.HumanUser.login", "is", "user2"]] + ) self.assertEqual(len(items), 2) - items = self._mockgun.find("PipelineConfiguration", [["users.HumanUser.login", "contains", "ser"]]) + items = self._mockgun.find( + "PipelineConfiguration", [["users.HumanUser.login", "contains", "ser"]] + ) self.assertEqual(len(items), 3) # Lets get fancy a bit. - items = self._mockgun.find("PipelineConfiguration", [{ - "filter_operator": "any", - "filters": [ - ["users.HumanUser.login", "is", "user1"], - ["users.HumanUser.login", "is", "user2"] - ]}] + items = self._mockgun.find( + "PipelineConfiguration", + [ + { + "filter_operator": "any", + "filters": [ + ["users.HumanUser.login", "is", "user1"], + ["users.HumanUser.login", "is", "user2"], + ], + } + ], ) self.assertEqual(len(items), 3) - items = self._mockgun.find("PipelineConfiguration", [{ - "filter_operator": "all", - "filters": [ - ["users.HumanUser.login", "is", "user1"], - ["users.HumanUser.login", "is", "user2"] - ]}] + items = self._mockgun.find( + "PipelineConfiguration", + [ + { + "filter_operator": "all", + "filters": [ + ["users.HumanUser.login", "is", "user1"], + ["users.HumanUser.login", "is", "user2"], + ], + } + ], ) self.assertEqual(len(items), 1) @@ -261,16 +286,20 @@ def test_find_with_none(self): """ Ensures comparison with multi-entity fields and None works. """ - items = self._mockgun.find("PipelineConfiguration", [["users", "is", None]], ["users"]) + items = self._mockgun.find( + "PipelineConfiguration", [["users", "is", None]], ["users"] + ) self.assertEqual(len(items), 1) self.assertEqual(items[0]["users"], []) - items = self._mockgun.find("PipelineConfiguration", [["users", "is_not", None]], ["users"]) + items = self._mockgun.find( + "PipelineConfiguration", [["users", "is_not", None]], ["users"] + ) self.assertEqual(len(items), 3) for item in items: self.assertTrue(len(item["users"]) > 0) - + class TestMultiEntityFieldUpdate(unittest.TestCase): """ Ensures multi entity field update modes work. @@ -281,13 +310,15 @@ def setUp(self): Creates test data. """ - self._mockgun = Mockgun("https://test.shotgunstudio.com", login="user", password="1234") + self._mockgun = Mockgun( + "https://test.shotgunstudio.com", login="user", password="1234" + ) # Create two versions to assign to the shot. self._version1 = self._mockgun.create("Version", {"code": "version1"}) self._version2 = self._mockgun.create("Version", {"code": "version2"}) self._version3 = self._mockgun.create("Version", {"code": "version3"}) - + # remove 'code' field for later comparisons del self._version1["code"] del self._version2["code"] @@ -296,15 +327,18 @@ def setUp(self): # Create playlists self._add_playlist = self._mockgun.create( "Playlist", - {"code": "playlist1", "versions": [self._version1, self._version2]} + {"code": "playlist1", "versions": [self._version1, self._version2]}, ) self._remove_playlist = self._mockgun.create( "Playlist", - {"code": "playlist1", "versions": [self._version1, self._version2, self._version3]} + { + "code": "playlist1", + "versions": [self._version1, self._version2, self._version3], + }, ) self._set_playlist = self._mockgun.create( "Playlist", - {"code": "playlist1", "versions": [self._version1, self._version2]} + {"code": "playlist1", "versions": [self._version1, self._version2]}, ) def test_update_add(self): @@ -312,8 +346,10 @@ def test_update_add(self): Ensures that "add" multi_entity_update_mode works. """ self._mockgun.update( - "Playlist", self._add_playlist["id"], {"versions": [self._version3]}, - multi_entity_update_modes={"versions": "add"} + "Playlist", + self._add_playlist["id"], + {"versions": [self._version3]}, + multi_entity_update_modes={"versions": "add"}, ) playlist = self._mockgun.find_one( @@ -328,8 +364,10 @@ def test_update_remove(self): Ensures that "remove" multi_entity_update_mode works. """ self._mockgun.update( - "Playlist", self._remove_playlist["id"], {"versions": [self._version2]}, - multi_entity_update_modes={"versions": "remove"} + "Playlist", + self._remove_playlist["id"], + {"versions": [self._version2]}, + multi_entity_update_modes={"versions": "remove"}, ) playlist = self._mockgun.find_one( @@ -345,14 +383,14 @@ def test_update_set(self): "Playlist", self._set_playlist["id"], {"versions": [self._version2, self._version3]}, - multi_entity_update_modes={"versions": "set"} + multi_entity_update_modes={"versions": "set"}, ) playlist = self._mockgun.find_one( "Playlist", [["id", "is", self._set_playlist["id"]]], ["versions"] ) self.assertEqual(playlist["versions"], [self._version2, self._version3]) - + def test_batch_update(self): self._mockgun.batch( [ @@ -361,7 +399,7 @@ def test_batch_update(self): "entity_type": "Playlist", "entity_id": self._set_playlist["id"], "data": {"versions": [self._version1, self._version2]}, - "multi_entity_update_modes": {"versions": "set"} + "multi_entity_update_modes": {"versions": "set"}, } ] ) @@ -382,44 +420,24 @@ def setUp(self): """ super(TestFilterOperator, self).setUp() - self._mockgun = Mockgun("https://test.shotgunstudio.com", login="user", password="1234") - - self._prj1_link = self._mockgun.create( - "Project", - { - "name": "prj1" - } + self._mockgun = Mockgun( + "https://test.shotgunstudio.com", login="user", password="1234" ) - self._prj2_link = self._mockgun.create( - "Project", - { - "name": "prj2" - } - ) + self._prj1_link = self._mockgun.create("Project", {"name": "prj1"}) + + self._prj2_link = self._mockgun.create("Project", {"name": "prj2"}) self._shot1 = self._mockgun.create( - "Shot", - { - "code": "shot1", - "project": self._prj1_link - } + "Shot", {"code": "shot1", "project": self._prj1_link} ) self._shot2 = self._mockgun.create( - "Shot", - { - "code": "shot2", - "project": self._prj1_link - } + "Shot", {"code": "shot2", "project": self._prj1_link} ) self._shot3 = self._mockgun.create( - "Shot", - { - "code": "shot3", - "project": self._prj2_link - } + "Shot", {"code": "shot3", "project": self._prj2_link} ) def test_simple_filter_operators(self): @@ -428,26 +446,24 @@ def test_simple_filter_operators(self): """ shots = self._mockgun.find( "Shot", - [{ - "filter_operator": "any", - "filters": [ - ["code", "is", "shot1"], - ["code", "is", "shot2"] - ] - }] + [ + { + "filter_operator": "any", + "filters": [["code", "is", "shot1"], ["code", "is", "shot2"]], + } + ], ) self.assertEqual(len(shots), 2) shots = self._mockgun.find( "Shot", - [{ - "filter_operator": "all", - "filters": [ - ["code", "is", "shot1"], - ["code", "is", "shot2"] - ] - }] + [ + { + "filter_operator": "all", + "filters": [["code", "is", "shot1"], ["code", "is", "shot2"]], + } + ], ) self.assertEqual(len(shots), 0) @@ -467,19 +483,19 @@ def test_nested_filter_operators(self): "filter_operator": "all", "filters": [ ["code", "is", "shot1"], - ["project", "is", self._prj1_link] - ] + ["project", "is", self._prj1_link], + ], }, { "filter_operator": "all", "filters": [ ["code", "is", "shot3"], - ["project", "is", self._prj2_link] - ] - } - ] + ["project", "is", self._prj2_link], + ], + }, + ], } - ] + ], ) self.assertEqual(len(shots), 2) @@ -490,24 +506,14 @@ def test_invalid_operator(self): ShotgunError, "Unknown filter_operator type: bad", lambda: self._mockgun.find( - "Shot", - [ - { - "filter_operator": "bad", - "filters": [] - } - ]) + "Shot", [{"filter_operator": "bad", "filters": []}] + ), ) self.assertRaisesRegex( ShotgunError, "Bad filter operator, requires keys 'filter_operator' and 'filters',", - lambda: self._mockgun.find( - "Shot", - [ - { - } - ]) + lambda: self._mockgun.find("Shot", [{}]), ) @@ -535,5 +541,5 @@ def test_set_server_params_with_url_with_path(self): self.assertEqual(mockgun.config.api_path, "/something/api3/json") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 85f70500a..cb713cd9d 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -15,22 +15,24 @@ class ServerConnectionTest(base.TestBase): - '''Tests for server connection''' + """Tests for server connection""" + def setUp(self): super(ServerConnectionTest, self).setUp() def test_connection(self): - '''Tests server connects and returns nothing''' + """Tests server connects and returns nothing""" result = self.sg.connect() self.assertEqual(result, None) def test_proxy_info(self): - '''check proxy value depending http_proxy setting in config''' + """check proxy value depending http_proxy setting in config""" self.sg.connect() if self.config.http_proxy: sys.stderr.write("[WITH PROXY] ") - self.assertTrue(isinstance(self.sg._connection.proxy_info, - api.lib.httplib2.ProxyInfo)) + self.assertTrue( + isinstance(self.sg._connection.proxy_info, api.lib.httplib2.ProxyInfo) + ) else: sys.stderr.write("[NO PROXY] ") self.assertEqual(self.sg._connection.proxy_info, None) diff --git a/tests/test_unit.py b/tests/test_unit.py index 84304cab7..de996c553 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -21,30 +21,35 @@ class TestShotgunInit(unittest.TestCase): - '''Test case for Shotgun.__init__''' + """Test case for Shotgun.__init__""" + def setUp(self): - self.server_path = 'http://server_path' - self.script_name = 'script_name' - self.api_key = 'api_key' + self.server_path = "http://server_path" + self.script_name = "script_name" + self.api_key = "api_key" # Proxy Server Tests def test_http_proxy_server(self): proxy_server = "someserver.com" http_proxy = proxy_server - sg = api.Shotgun(self.server_path, - self.script_name, - self.api_key, - http_proxy=http_proxy, - connect=False) + sg = api.Shotgun( + self.server_path, + self.script_name, + self.api_key, + http_proxy=http_proxy, + connect=False, + ) self.assertEqual(sg.config.proxy_server, proxy_server) self.assertEqual(sg.config.proxy_port, 8080) proxy_server = "123.456.789.012" http_proxy = proxy_server - sg = api.Shotgun(self.server_path, - self.script_name, - self.api_key, - http_proxy=http_proxy, - connect=False) + sg = api.Shotgun( + self.server_path, + self.script_name, + self.api_key, + http_proxy=http_proxy, + connect=False, + ) self.assertEqual(sg.config.proxy_server, proxy_server) self.assertEqual(sg.config.proxy_port, 8080) @@ -52,21 +57,25 @@ def test_http_proxy_server_and_port(self): proxy_server = "someserver.com" proxy_port = 1234 http_proxy = "%s:%d" % (proxy_server, proxy_port) - sg = api.Shotgun(self.server_path, - self.script_name, - self.api_key, - http_proxy=http_proxy, - connect=False) + sg = api.Shotgun( + self.server_path, + self.script_name, + self.api_key, + http_proxy=http_proxy, + connect=False, + ) self.assertEqual(sg.config.proxy_server, proxy_server) self.assertEqual(sg.config.proxy_port, proxy_port) proxy_server = "123.456.789.012" proxy_port = 1234 http_proxy = "%s:%d" % (proxy_server, proxy_port) - sg = api.Shotgun(self.server_path, - self.script_name, - self.api_key, - http_proxy=http_proxy, - connect=False) + sg = api.Shotgun( + self.server_path, + self.script_name, + self.api_key, + http_proxy=http_proxy, + connect=False, + ) self.assertEqual(sg.config.proxy_server, proxy_server) self.assertEqual(sg.config.proxy_port, proxy_port) @@ -75,13 +84,14 @@ def test_http_proxy_server_and_port_with_authentication(self): proxy_port = 1234 proxy_user = "user" proxy_pass = "password" - http_proxy = "%s:%s@%s:%d" % (proxy_user, proxy_pass, proxy_server, - proxy_port) - sg = api.Shotgun(self.server_path, - self.script_name, - self.api_key, - http_proxy=http_proxy, - connect=False) + http_proxy = "%s:%s@%s:%d" % (proxy_user, proxy_pass, proxy_server, proxy_port) + sg = api.Shotgun( + self.server_path, + self.script_name, + self.api_key, + http_proxy=http_proxy, + connect=False, + ) self.assertEqual(sg.config.proxy_server, proxy_server) self.assertEqual(sg.config.proxy_port, proxy_port) self.assertEqual(sg.config.proxy_user, proxy_user) @@ -90,13 +100,14 @@ def test_http_proxy_server_and_port_with_authentication(self): proxy_port = 1234 proxy_user = "user" proxy_pass = "password" - http_proxy = "%s:%s@%s:%d" % (proxy_user, proxy_pass, proxy_server, - proxy_port) - sg = api.Shotgun(self.server_path, - self.script_name, - self.api_key, - http_proxy=http_proxy, - connect=False) + http_proxy = "%s:%s@%s:%d" % (proxy_user, proxy_pass, proxy_server, proxy_port) + sg = api.Shotgun( + self.server_path, + self.script_name, + self.api_key, + http_proxy=http_proxy, + connect=False, + ) self.assertEqual(sg.config.proxy_server, proxy_server) self.assertEqual(sg.config.proxy_port, proxy_port) self.assertEqual(sg.config.proxy_user, proxy_user) @@ -107,13 +118,14 @@ def test_http_proxy_with_at_in_password(self): proxy_port = 1234 proxy_user = "user" proxy_pass = "p@ssword" - http_proxy = "%s:%s@%s:%d" % (proxy_user, proxy_pass, proxy_server, - proxy_port) - sg = api.Shotgun(self.server_path, - self.script_name, - self.api_key, - http_proxy=http_proxy, - connect=False) + http_proxy = "%s:%s@%s:%d" % (proxy_user, proxy_pass, proxy_server, proxy_port) + sg = api.Shotgun( + self.server_path, + self.script_name, + self.api_key, + http_proxy=http_proxy, + connect=False, + ) self.assertEqual(sg.config.proxy_server, proxy_server) self.assertEqual(sg.config.proxy_port, proxy_port) self.assertEqual(sg.config.proxy_user, proxy_user) @@ -121,63 +133,63 @@ def test_http_proxy_with_at_in_password(self): def test_malformatted_proxy_info(self): conn_info = { - 'base_url': self.server_path, - 'script_name': self.script_name, - 'api_key': self.api_key, - 'connect': False, + "base_url": self.server_path, + "script_name": self.script_name, + "api_key": self.api_key, + "connect": False, } - conn_info['http_proxy'] = 'http://someserver.com' + conn_info["http_proxy"] = "http://someserver.com" self.assertRaises(ValueError, api.Shotgun, **conn_info) - conn_info['http_proxy'] = 'user@someserver.com' + conn_info["http_proxy"] = "user@someserver.com" self.assertRaises(ValueError, api.Shotgun, **conn_info) - conn_info['http_proxy'] = 'someserver.com:1234:5678' + conn_info["http_proxy"] = "someserver.com:1234:5678" self.assertRaises(ValueError, api.Shotgun, **conn_info) class TestShotgunSummarize(unittest.TestCase): - '''Test case for _create_summary_request function and parameter + """Test case for _create_summary_request function and parameter validation as it exists in Shotgun.summarize. - Does not require database connection or test data.''' + Does not require database connection or test data.""" + def setUp(self): - self.sg = api.Shotgun('http://server_path', - 'script_name', - 'api_key', - connect=False) + self.sg = api.Shotgun( + "http://server_path", "script_name", "api_key", connect=False + ) def test_filter_operator_none(self): - expected_logical_operator = 'and' + expected_logical_operator = "and" filter_operator = None self._assert_filter_operator(expected_logical_operator, filter_operator) def _assert_filter_operator(self, expected_logical_operator, filter_operator): - result = self.get_call_rpc_params(None, {'filter_operator': filter_operator}) - actual_logical_operator = result['filters']['logical_operator'] + result = self.get_call_rpc_params(None, {"filter_operator": filter_operator}) + actual_logical_operator = result["filters"]["logical_operator"] self.assertEqual(expected_logical_operator, actual_logical_operator) def test_filter_operator_all(self): - expected_logical_operator = 'and' - filter_operator = 'all' + expected_logical_operator = "and" + filter_operator = "all" self._assert_filter_operator(expected_logical_operator, filter_operator) def test_filter_operator_or(self): - expected_logical_operator = 'or' - filter_operator = 'or' + expected_logical_operator = "or" + filter_operator = "or" self._assert_filter_operator(expected_logical_operator, filter_operator) def test_filters(self): - path = 'path' - relation = 'relation' - value = 'value' - expected_condition = {'path': path, 'relation': relation, 'values': [value]} - args = ['', [[path, relation, value]], None] + path = "path" + relation = "relation" + value = "value" + expected_condition = {"path": path, "relation": relation, "values": [value]} + args = ["", [[path, relation, value]], None] result = self.get_call_rpc_params(args, {}) - actual_condition = result['filters']['conditions'][0] + actual_condition = result["filters"]["conditions"][0] self.assertEqual(expected_condition, actual_condition) - @patch('shotgun_api3.Shotgun._call_rpc') + @patch("shotgun_api3.Shotgun._call_rpc") def get_call_rpc_params(self, args, kws, call_rpc): - '''Return params sent to _call_rpc from summarize.''' + """Return params sent to _call_rpc from summarize.""" if not args: args = [None, [], None] self.sg.summarize(*args, **kws) @@ -185,62 +197,72 @@ def get_call_rpc_params(self, args, kws, call_rpc): def test_grouping(self): result = self.get_call_rpc_params(None, {}) - self.assertFalse('grouping' in result) - grouping = ['something'] - kws = {'grouping': grouping} + self.assertFalse("grouping" in result) + grouping = ["something"] + kws = {"grouping": grouping} result = self.get_call_rpc_params(None, kws) - self.assertEqual(grouping, result['grouping']) + self.assertEqual(grouping, result["grouping"]) def test_grouping_type(self): - '''test_grouping_type tests that grouping parameter is a list or None''' - self.assertRaises(ValueError, self.sg.summarize, '', [], [], grouping='Not a list') + """test_grouping_type tests that grouping parameter is a list or None""" + self.assertRaises( + ValueError, self.sg.summarize, "", [], [], grouping="Not a list" + ) class TestShotgunBatch(unittest.TestCase): def setUp(self): - self.sg = api.Shotgun('http://server_path', - 'script_name', - 'api_key', - connect=False) + self.sg = api.Shotgun( + "http://server_path", "script_name", "api_key", connect=False + ) def test_missing_required_key(self): req = {} # requires keys request_type and entity_type self.assertRaises(api.ShotgunError, self.sg.batch, [req]) - req['entity_type'] = 'Entity' + req["entity_type"] = "Entity" self.assertRaises(api.ShotgunError, self.sg.batch, [req]) - req['request_type'] = 'not_real_type' + req["request_type"] = "not_real_type" self.assertRaises(api.ShotgunError, self.sg.batch, [req]) # create requires data key - req['request_type'] = 'create' + req["request_type"] = "create" self.assertRaises(api.ShotgunError, self.sg.batch, [req]) # update requires entity_id and data - req['request_type'] = 'update' - req['data'] = {} + req["request_type"] = "update" + req["data"] = {} self.assertRaises(api.ShotgunError, self.sg.batch, [req]) - del req['data'] - req['entity_id'] = 2334 + del req["data"] + req["entity_id"] = 2334 self.assertRaises(api.ShotgunError, self.sg.batch, [req]) # delete requires entity_id - req['request_type'] = 'delete' - del req['entity_id'] + req["request_type"] = "delete" + del req["entity_id"] self.assertRaises(api.ShotgunError, self.sg.batch, [req]) class TestServerCapabilities(unittest.TestCase): def test_no_server_version(self): - self.assertRaises(api.ShotgunError, api.shotgun.ServerCapabilities, 'host', {}) + self.assertRaises(api.ShotgunError, api.shotgun.ServerCapabilities, "host", {}) def test_bad_version(self): - '''test_bad_meta tests passing bad meta data type''' - self.assertRaises(api.ShotgunError, api.shotgun.ServerCapabilities, 'host', {'version': (0, 0, 0)}) + """test_bad_meta tests passing bad meta data type""" + self.assertRaises( + api.ShotgunError, + api.shotgun.ServerCapabilities, + "host", + {"version": (0, 0, 0)}, + ) def test_dev_version(self): - serverCapabilities = api.shotgun.ServerCapabilities('host', {'version': (3, 4, 0, 'Dev')}) + serverCapabilities = api.shotgun.ServerCapabilities( + "host", {"version": (3, 4, 0, "Dev")} + ) self.assertEqual(serverCapabilities.version, (3, 4, 0)) self.assertTrue(serverCapabilities.is_dev) - serverCapabilities = api.shotgun.ServerCapabilities('host', {'version': (2, 4, 0)}) + serverCapabilities = api.shotgun.ServerCapabilities( + "host", {"version": (2, 4, 0)} + ) self.assertEqual(serverCapabilities.version, (2, 4, 0)) self.assertFalse(serverCapabilities.is_dev) @@ -248,13 +270,13 @@ def test_dev_version(self): class TestClientCapabilities(unittest.TestCase): def test_darwin(self): - self.assert_platform('Darwin', 'mac') + self.assert_platform("Darwin", "mac") def test_windows(self): - self.assert_platform('win32', 'windows') + self.assert_platform("win32", "windows") def test_linux(self): - self.assert_platform('Linux', 'linux') + self.assert_platform("Linux", "linux") def assert_platform(self, sys_ret_val, expected): platform = api.shotgun.sys.platform @@ -278,12 +300,12 @@ def test_no_platform(self): finally: api.shotgun.sys.platform = platform - @patch('shotgun_api3.shotgun.sys') + @patch("shotgun_api3.shotgun.sys") def test_py_version(self, mock_sys): major = 2 minor = 7 micro = 3 - mock_sys.version_info = (major, minor, micro, 'final', 0) + mock_sys.version_info = (major, minor, micro, "final", 0) expected_py_version = "%s.%s" % (major, minor) client_caps = api.shotgun.ClientCapabilities() self.assertEqual(client_caps.py_version, expected_py_version) @@ -293,26 +315,20 @@ class TestFilters(unittest.TestCase): maxDiff = None def test_empty(self): - expected = { - "logical_operator": "and", - "conditions": [] - } + expected = {"logical_operator": "and", "conditions": []} result = api.shotgun._translate_filters([], None) self.assertEqual(result, expected) def test_simple(self): - filters = [ - ["code", "is", "test"], - ["sg_status_list", "is", "ip"] - ] + filters = [["code", "is", "test"], ["sg_status_list", "is", "ip"]] expected = { "logical_operator": "or", "conditions": [ {"path": "code", "relation": "is", "values": ["test"]}, - {"path": "sg_status_list", "relation": "is", "values": ["ip"]} - ] + {"path": "sg_status_list", "relation": "is", "values": ["ip"]}, + ], } result = api.shotgun._translate_filters(filters, "any") @@ -323,20 +339,20 @@ def test_arrays(self): expected = { "logical_operator": "and", "conditions": [ - {"path": "code", "relation": "in", "values": ["test1", "test2", "test3"]} - ] + { + "path": "code", + "relation": "in", + "values": ["test1", "test2", "test3"], + } + ], } - filters = [ - ["code", "in", "test1", "test2", "test3"] - ] + filters = [["code", "in", "test1", "test2", "test3"]] result = api.shotgun._translate_filters(filters, "all") self.assertEqual(result, expected) - filters = [ - ["code", "in", ["test1", "test2", "test3"]] - ] + filters = [["code", "in", ["test1", "test2", "test3"]]] result = api.shotgun._translate_filters(filters, "all") self.assertEqual(result, expected) @@ -353,11 +369,11 @@ def test_nested(self): "filter_operator": "all", "filters": [ ["sg_status_list", "is", "hld"], - ["assets", "is", {"type": "Asset", "id": 9}] - ] - } - ] - } + ["assets", "is", {"type": "Asset", "id": 9}], + ], + }, + ], + }, ] expected = { @@ -372,13 +388,21 @@ def test_nested(self): { "logical_operator": "and", "conditions": [ - {"path": "sg_status_list", "relation": "is", "values": ["hld"]}, - {"path": "assets", "relation": "is", "values": [{"type": "Asset", "id": 9}]}, - ] - } - ] - } - ] + { + "path": "sg_status_list", + "relation": "is", + "values": ["hld"], + }, + { + "path": "assets", + "relation": "is", + "values": [{"type": "Asset", "id": 9}], + }, + ], + }, + ], + }, + ], } result = api.shotgun._translate_filters(filters, "all") @@ -386,27 +410,27 @@ def test_nested(self): def test_invalid(self): self.assertRaises(api.ShotgunError, api.shotgun._translate_filters, [], "bogus") - self.assertRaises(api.ShotgunError, api.shotgun._translate_filters, ["bogus"], "all") + self.assertRaises( + api.ShotgunError, api.shotgun._translate_filters, ["bogus"], "all" + ) - filters = [{ - "filter_operator": "bogus", - "filters": [] - }] + filters = [{"filter_operator": "bogus", "filters": []}] - self.assertRaises(api.ShotgunError, api.shotgun._translate_filters, filters, "all") + self.assertRaises( + api.ShotgunError, api.shotgun._translate_filters, filters, "all" + ) - filters = [{ - "filters": [] - }] + filters = [{"filters": []}] - self.assertRaises(api.ShotgunError, api.shotgun._translate_filters, filters, "all") + self.assertRaises( + api.ShotgunError, api.shotgun._translate_filters, filters, "all" + ) - filters = [{ - "filter_operator": "all", - "filters": {"bogus": "bogus"} - }] + filters = [{"filter_operator": "all", "filters": {"bogus": "bogus"}}] - self.assertRaises(api.ShotgunError, api.shotgun._translate_filters, filters, "all") + self.assertRaises( + api.ShotgunError, api.shotgun._translate_filters, filters, "all" + ) @mock.patch.dict(os.environ, {"SHOTGUN_API_DISABLE_ENTITY_OPTIMIZATION": "1"}) def test_related_object(self): @@ -414,7 +438,13 @@ def test_related_object(self): [ "project", "is", - {"foo": "foo", "bar": "bar", "id": 999, "baz": "baz", "type": "Anything"}, + { + "foo": "foo", + "bar": "bar", + "id": 999, + "baz": "baz", + "type": "Anything", + }, ], ] expected = { @@ -445,7 +475,13 @@ def test_related_object_entity_optimization_is(self): [ "project", "is", - {"foo": "foo", "bar": "bar", "id": 999, "baz": "baz", "type": "Anything"}, + { + "foo": "foo", + "bar": "bar", + "id": 999, + "baz": "baz", + "type": "Anything", + }, ], ] expected = { @@ -481,7 +517,7 @@ def test_related_object_entity_optimization_is(self): { "path": "something", "relation": "is", - "values": [{'bar': 'bar', 'foo': 'foo'}], + "values": [{"bar": "bar", "foo": "foo"}], } ], } @@ -496,8 +532,20 @@ def test_related_object_entity_optimization_in(self): "project", "in", [ - {"foo1": "foo1", "bar1": "bar1", "id": 999, "baz1": "baz1", "type": "Anything"}, - {"foo2": "foo2", "bar2": "bar2", "id": 998, "baz2": "baz2", "type": "Anything"}, + { + "foo1": "foo1", + "bar1": "bar1", + "id": 999, + "baz1": "baz1", + "type": "Anything", + }, + { + "foo2": "foo2", + "bar2": "bar2", + "id": 998, + "baz2": "baz2", + "type": "Anything", + }, {"foo3": "foo3", "bar3": "bar3"}, ], ], @@ -520,7 +568,7 @@ def test_related_object_entity_optimization_in(self): { "foo3": "foo3", "bar3": "bar3", - } + }, ], } ], @@ -560,7 +608,9 @@ def test_related_object_update_entity(self): ], } sg = api.Shotgun("http://server_path", "script_name", "api_key", connect=False) - result = sg._translate_update_params(entity_type, entity_id, data, multi_entity_update_modes) + result = sg._translate_update_params( + entity_type, entity_id, data, multi_entity_update_modes + ) self.assertEqual(result, expected) @mock.patch("shotgun_api3.shotgun.SHOTGUN_API_DISABLE_ENTITY_OPTIMIZATION", False) @@ -611,7 +661,9 @@ def test_related_object_update_optimization_entity(self): ], } sg = api.Shotgun("http://server_path", "script_name", "api_key", connect=False) - result = sg._translate_update_params(entity_type, entity_id, data, multi_entity_update_modes) + result = sg._translate_update_params( + entity_type, entity_id, data, multi_entity_update_modes + ) self.assertEqual(result, expected) @mock.patch("shotgun_api3.shotgun.SHOTGUN_API_DISABLE_ENTITY_OPTIMIZATION", False) @@ -625,7 +677,11 @@ def test_related_object_update_optimization_entity_multi(self): {"id": 6441, "type": "Asset", "name": "disposable name 6441"}, {"id": 6440, "type": "Asset"}, ], - "sg_class": {"id": 1, "type": "CustomEntity53", "name": "disposable name 1"}, + "sg_class": { + "id": 1, + "type": "CustomEntity53", + "name": "disposable name 1", + }, } expected = { "type": "Asset", @@ -640,7 +696,10 @@ def test_related_object_update_optimization_entity_multi(self): {"id": 6440, "type": "Asset"}, ], }, - {"field_name": "sg_class", "value": {"type": "CustomEntity53", "id": 1}}, + { + "field_name": "sg_class", + "value": {"type": "CustomEntity53", "id": 1}, + }, ], } sg = api.Shotgun("http://server_path", "script_name", "api_key", connect=False) @@ -662,10 +721,9 @@ class TestCerts(unittest.TestCase): ] def setUp(self): - self.sg = api.Shotgun('http://server_path', - 'script_name', - 'api_key', - connect=False) + self.sg = api.Shotgun( + "http://server_path", "script_name", "api_key", connect=False + ) # Get the location of the certs file self.certs = self.sg._get_certs_file(None) @@ -712,7 +770,12 @@ def test_httplib(self): certificate with httplib. """ # First check that we get an error when trying to connect to a known dummy bad URL - self.assertRaises(ssl_error_classes, self._check_url_with_sg_api_httplib2, self.bad_url, self.certs) + self.assertRaises( + ssl_error_classes, + self._check_url_with_sg_api_httplib2, + self.bad_url, + self.certs, + ) # Now check that the good urls connect properly using the certs for url in self.test_urls: @@ -725,12 +788,14 @@ def test_urlib(self): certificate with urllib. """ # First check that we get an error when trying to connect to a known dummy bad URL - self.assertRaises(urllib.error.URLError, self._check_url_with_urllib, self.bad_url) + self.assertRaises( + urllib.error.URLError, self._check_url_with_urllib, self.bad_url + ) # Now check that the good urls connect properly using the certs for url in self.test_urls: response = self._check_url_with_urllib(url) - assert (response is not None) + assert response is not None class TestMimetypesFix(unittest.TestCase): @@ -738,8 +803,10 @@ class TestMimetypesFix(unittest.TestCase): Makes sure that the mimetypes fix will be imported. """ - @patch('shotgun_api3.shotgun.sys') - def _test_mimetypes_import(self, platform, major, minor, patch_number, result, mock): + @patch("shotgun_api3.shotgun.sys") + def _test_mimetypes_import( + self, platform, major, minor, patch_number, result, mock + ): """ Mocks sys.platform and sys.version_info to test the mimetypes import code. """ @@ -749,5 +816,5 @@ def _test_mimetypes_import(self, platform, major, minor, patch_number, result, m self.assertEqual(_is_mimetypes_broken(), result) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/update_httplib2.py b/update_httplib2.py index acfdee2b7..30422e0c2 100755 --- a/update_httplib2.py +++ b/update_httplib2.py @@ -17,18 +17,20 @@ class Utilities: def download_archive(self, file_path, file_name): """Download the archive from github.""" print(f"Downloading {file_name}") - subprocess.check_output([ - "curl", - "-L", - f"https://github.com/httplib2/httplib2/archive/{file_name}", - "-o", - file_path]) + subprocess.check_output( + [ + "curl", + "-L", + f"https://github.com/httplib2/httplib2/archive/{file_name}", + "-o", + file_path, + ] + ) def unzip_archive(self, file_path, file_name, temp_dir): """Unzip in a temp dir.""" print(f"Unzipping {file_name}") - subprocess.check_output( - ["unzip", str(file_path), "-d", str(temp_dir)]) + subprocess.check_output(["unzip", str(file_path), "-d", str(temp_dir)]) def remove_folder(self, path): """Remove a folder recursively.""" @@ -38,11 +40,14 @@ def remove_folder(self, path): def git_remove(self, target): print(f"Removing {target} in git.") try: - subprocess.check_output([ - "git", - "rm", - "-rf", - ] + target) + subprocess.check_output( + [ + "git", + "rm", + "-rf", + ] + + target + ) except Exception as e: pass @@ -58,8 +63,8 @@ def sanitize_file(self, file_path): contents = contents.replace("from httplib2.", "from .") contents = contents.replace("from httplib2", "from .") contents = contents.replace( - "import pyparsing as pp", - "from ... import pyparsing as pp") + "import pyparsing as pp", "from ... import pyparsing as pp" + ) with open(file_path, "w") as f: f.write(contents) @@ -88,18 +93,13 @@ def main(temp_path, repo_root, version): utilities.remove_folder(python3_dir) # Removes the previous version of httplib2 - utilities.git_remove([ - str(python2_dir), - str(python3_dir) - ]) + utilities.git_remove([str(python2_dir), str(python3_dir)]) # Copies a new version into place. print("Copying new version of httplib2") root_folder = unzipped_folder / f"httplib2-{version[1:]}" - utilities.copy_folder( - str(root_folder / "python2" / "httplib2"), python2_dir) - utilities.copy_folder( - str(root_folder / "python3" / "httplib2"), python3_dir) + utilities.copy_folder(str(root_folder / "python2" / "httplib2"), python2_dir) + utilities.copy_folder(str(root_folder / "python3" / "httplib2"), python3_dir) utilities.remove_folder(f"{python2_dir}/test") utilities.remove_folder(f"{python3_dir}/test")