diff --git a/shotgun_api3/lib/httplib2/__init__.py b/shotgun_api3/lib/httplib2/__init__.py index 19e7cff11..5c5e5e5f3 100644 --- a/shotgun_api3/lib/httplib2/__init__.py +++ b/shotgun_api3/lib/httplib2/__init__.py @@ -827,7 +827,7 @@ def proxy_info_from_environment(method='http'): def proxy_info_from_url(url, method='http'): """ - Construct a ProxyInfo from a URL (such as http_proxy env var) + Construct a ProxyInfo from a URL (such as http_proxy fj var) """ url = urlparse.urlparse(url) username = None diff --git a/shotgun_api3/lib/mockgun/mockgun.py b/shotgun_api3/lib/mockgun/mockgun.py index e0ab26d78..112c0d8c5 100644 --- a/shotgun_api3/lib/mockgun/mockgun.py +++ b/shotgun_api3/lib/mockgun/mockgun.py @@ -32,56 +32,56 @@ ----------------------------------------------------------------------------- -M O C K G U N +M O C K G U N ----------------------------------------------------------------------------- Experimental software ahead! ---------------------------- -Disclaimer! Mockgun is in its early stages of development. It is not fully -compatible with the Shotgun API yet and we offer no guarantees at this point -that future versions of Mockgun will be backwards compatible. Consider this -alpha level software and use at your own risk. +Disclaimer! Mockgun is in its early stages of development. It is not fully +compatible with the Shotgun API yet and we offer no guarantees at this point +that future versions of Mockgun will be backwards compatible. Consider this +alpha level software and use at your own risk. What is Mockgun? ---------------- Mockgun is a Shotgun API mocker. It's a class that has got *most* of the same -methods and parameters that the Shotgun API has got. Mockgun is essentially a +methods and parameters that the Shotgun API has got. Mockgun is essentially a Shotgun *emulator* that (for basic operations) looks and feels like Shotgun. The primary purpose of Mockgun is to drive unit test rigs where it becomes -too slow, cumbersome or non-practical to connect to a real Shotgun. Using a -Mockgun for unit tests means that a test can be rerun over and over again +too slow, cumbersome or non-practical to connect to a real Shotgun. Using a +Mockgun for unit tests means that a test can be rerun over and over again from exactly the same database state. This can be hard to do if you connect -to a live Shotgun instance. +to a live Shotgun instance. How do I use Mockgun? --------------------- First of all, you need a Shotgun schema to run against. This will define -all the fields and entities that mockgun will use. Simply connect to +all the fields and entities that mockgun will use. Simply connect to your Shotgun site and use the generate_schema() method to download the schema data: # connect to your site from shotgun_api3 import Shotgun sg = Shotgun("https://mysite.shotgunstudio.com", script_name="xyz", api_key="abc") - + # write out schema data to files from shotgun_api3.lib import mockgun mockgun.generate_schema(sg, "/tmp/schema", "/tmp/entity_schema") - + Now that you have a schema, you can tell your mockgun instance about it. -We do this as a class-level operation, so that the consctructor can be +We do this as a class-level operation, so that the consctructor can be exactly like the real Shotgun one: from shotgun_api3.lib import mockgun - + # tell mockgun about the schema mockgun.Shotgun.set_schema_paths("/tmp/schema", "/tmp/entity_schema") - - # we are ready to mock! - # this call will not connect to mysite, but instead create a + + # we are ready to mock! + # this call will not connect to mysite, but instead create a # mockgun instance which is connected to an *empty* shotgun site # which has got the same schema as mysite. sg = mockgun.Shotgun("https://mysite.shotgunstudio.com", script_name="xyz", api_key="abc") @@ -89,15 +89,15 @@ # now you can start putting stuff in print sg.create("HumanUser", {"firstname": "John", "login": "john"}) # prints {'login': 'john', 'type': 'HumanUser', 'id': 1, 'firstname': 'John'} - + # and find what you have created print sg.find("HumanUser", [["login", "is", "john"]]) prints [{'type': 'HumanUser', 'id': 1}] - + That's it! Mockgun is used to run the Shotgun Pipeline Toolkit unit test rig. Mockgun has a 'database' in the form of a dictionary stored in Mockgun._db -By editing this directly, you can modify the database without going through +By editing this directly, you can modify the database without going through the API. @@ -126,6 +126,45 @@ __version__ = "0.0.1" +<<<<<<< HEAD:shotgun_api3/lib/mockgun/mockgun.py +======= +class MockgunError(Exception): + """ + Base for all Mockgun related API Errors. + These are errors that relate to mockgun specifically, for example + relating to mockups setup and initialization. For operational errors, + mockgun raises ShotgunErrors just like the Shotgun API. + """ + pass + +# ---------------------------------------------------------------------------- +# Utility methods + +def generate_schema(shotgun, schema_file_path, schema_entity_file_path): + """ + Helper method for mockgun. + Generates the schema files needed by the mocker by connecting to a real shotgun_repos + and downloading the schema information for that site. Once the generated schema + files are being passed to mockgun, it will mimic the site's schema structure. + + :param sg_url: Shotgun site url + :param sg_script: Script name to connect with + :param sg_key: Script key to connect with + :param schema_file_path: Path where to write the main schema file to + :param schema_entity_file_path: Path where to write the entity schema file to + """ + + schema = shotgun.schema_read() + fh = open(schema_file_path, "w") + pickle.dump(schema, fh) + fh.close() + + schema_entity = shotgun.schema_entity_read() + fh = open(schema_entity_file_path, "w") + pickle.dump(schema_entity, fh) + fh.close() + +>>>>>>> origin/master:shotgun_api3/lib/mockgun.py # ---------------------------------------------------------------------------- # API @@ -135,13 +174,13 @@ class Shotgun(object): It generates an object which looks and feels like a normal Shotgun API instance. Instead of connecting to a real server, it keeps all its data in memory in a way which makes it easy to introspect and test. - + The methods presented in this class reflect the Shotgun API and are therefore sparsely documented. - + Please note that this class is built for test purposes only and only creates an - object which *roughly* resembles the Shotgun API - however, for most common - use cases, this is enough to be able to perform relevant and straight forward + object which *roughly* resembles the Shotgun API - however, for most common + use cases, this is enough to be able to perform relevant and straight forward testing of code. """ @@ -155,7 +194,7 @@ def set_schema_paths(cls, schema_path, schema_entity_path): level so all Shotgun instances will share the same schema. The responsability to generate and load these files is left to the user changing the default value. - + :param schema_path: Directory path where schema files are. """ cls.__schema_path = schema_path @@ -166,7 +205,7 @@ def get_schema_paths(cls): """ Returns a tuple with paths to the files which are part of the schema. These paths can then be used in generate_schema if needed. - + :returns: A tuple with schema_file_path and schema_entity_file_path """ return (cls.__schema_path, cls.__schema_entity_path) @@ -191,23 +230,44 @@ def __init__(self, # having them present means code and get and set them # they way they would expect to in the real API. self.config = _Config() - - # load in the shotgun schema to associate with this Shotgun + + # load in the shotgun_repos schema to associate with this Shotgun (schema_path, schema_entity_path) = self.get_schema_paths() if schema_path is None or schema_entity_path is None: raise MockgunError("Cannot create Mockgun instance because no schema files have been defined. " "Before creating a Mockgun instance, please call Mockgun.set_schema_paths() " "in order to specify which Shotgun schema Mockgun should operate against.") +<<<<<<< HEAD:shotgun_api3/lib/mockgun/mockgun.py self._schema, self._schema_entity = SchemaFactory.get_schemas(schema_path, schema_entity_path) +======= + + if not os.path.exists(schema_path): + raise MockgunError("Cannot locate Mockgun schema file '%s'!" % schema_path) + + if not os.path.exists(schema_entity_path): + raise MockgunError("Cannot locate Mockgun schema file '%s'!" % schema_entity_path) + + fh = open(schema_path, "r") + try: + self._schema = pickle.load(fh) + finally: + fh.close() + + fh = open(schema_entity_path, "r") + try: + self._schema_entity = pickle.load(fh) + finally: + fh.close() +>>>>>>> origin/master:shotgun_api3/lib/mockgun.py # initialize the "database" self._db = dict((entity, {}) for entity in self._schema) # set some basic public members that exist in the Shotgun API self.base_url = base_url - + # bootstrap the event log # let's make sure there is at least one event log id in our mock db data = {} @@ -226,13 +286,13 @@ def schema_read(self): def schema_field_create(self, entity_type, data_type, display_name, properties=None): raise NotImplementedError - + def schema_field_update(self, entity_type, field_name, properties): raise NotImplementedError def schema_field_delete(self, entity_type, field_name): raise NotImplementedError - + def schema_entity_read(self): return self._schema_entity @@ -244,7 +304,7 @@ def schema_field_read(self, entity_type, field_name=None): def find(self, entity_type, filters, fields=None, order=None, filter_operator=None, limit=0, retired_only=False, page=0): - + self.finds += 1 @@ -311,12 +371,12 @@ def find(self, entity_type, filters, fields=None, order=None, filter_operator=No val = [dict((field, self._get_field_from_row(entity_type, row, field)) for field in fields) for row in results] return val - - + + def find_one(self, entity_type, filters, fields=None, order=None, filter_operator=None, retired_only=False): results = self.find(entity_type, filters, fields=fields, order=order, filter_operator=filter_operator, retired_only=retired_only) return results[0] if results else None - + def batch(self, requests): results = [] for request in requests: @@ -332,15 +392,15 @@ def batch(self, requests): return results def create(self, entity_type, data, return_fields=None): - + # special handling of storage fields - if a field value - # is a dict with a key local_path, then add fields - # local_path_linux, local_path_windows, local_path_mac + # is a dict with a key local_path, then add fields + # local_path_linux, local_path_windows, local_path_mac # as a reflection of this for d in data: if isinstance(data[d], dict) and "local_path" in data[d]: - # partly imitate some of the business logic happening on the - # server side of shotgun when a file/link entity value is created + # partly imitate some of the business logic happening on the + # server side of shotgun_repos when a file/link entity value is created if "local_storage" not in data[d]: data[d]["local_storage"] = {"id": 0, "name": "auto_generated_by_mockgun", "type": "LocalStorage"} if "local_path_linux" not in data[d]: @@ -349,7 +409,7 @@ def create(self, entity_type, data, return_fields=None): data[d]["local_path_windows"] = data[d]["local_path"] if "local_path_mac" not in data[d]: data[d]["local_path_mac"] = data[d]["local_path"] - + self._validate_entity_type(entity_type) self._validate_entity_data(entity_type, data) self._validate_entity_fields(entity_type, return_fields) @@ -358,14 +418,14 @@ def create(self, entity_type, data, return_fields=None): next_id = max(self._db[entity_type]) + 1 except ValueError: next_id = 1 - + row = self._get_new_row(entity_type) - - self._update_row(entity_type, row, data) + + self._update_row(entity_type, row, data) row["id"] = next_id - + self._db[entity_type][next_id] = row - + if return_fields is None: result = dict((field, self._get_field_from_row(entity_type, row, field)) for field in data) else: @@ -373,7 +433,7 @@ def create(self, entity_type, data, return_fields=None): result["type"] = row["type"] result["id"] = row["id"] - + return result def update(self, entity_type, entity_id, data): @@ -389,28 +449,28 @@ def update(self, entity_type, entity_id, data): def delete(self, entity_type, entity_id): self._validate_entity_type(entity_type) self._validate_entity_exists(entity_type, entity_id) - + row = self._db[entity_type][entity_id] if not row["__retired"]: row["__retired"] = True return True else: return False - + def revive(self, entity_type, entity_id): self._validate_entity_type(entity_type) self._validate_entity_exists(entity_type, entity_id) - + row = self._db[entity_type][entity_id] if row["__retired"]: row["__retired"] = False return True else: return False - + def upload(self, entity_type, entity_id, path, field_name=None, display_name=None, tag_list=None): raise NotImplementedError - + def upload_thumbnail(self, entity_type, entity_id, path, **kwargs): pass @@ -420,7 +480,7 @@ def upload_thumbnail(self, entity_type, entity_id, path, **kwargs): def _validate_entity_type(self, entity_type): if entity_type not in self._schema: raise ShotgunError("%s is not a valid entity" % entity_type) - + def _validate_entity_data(self, entity_type, data): if "id" in data or "type" in data: raise ShotgunError("Can't set id or type on create or update") @@ -428,11 +488,11 @@ def _validate_entity_data(self, entity_type, data): self._validate_entity_fields(entity_type, data.keys()) for field, item in data.items(): - + if item is None: # none is always ok continue - + field_info = self._schema[entity_type][field] if field_info["data_type"]["value"] == "multi_entity": @@ -444,8 +504,8 @@ def _validate_entity_data(self, entity_type, data): raise ShotgunError("%s.%s is of type multi-entity, but an item in data %s does not contain 'type' and 'id'" % (entity_type, field, item)) elif item and any(sub_item["type"] not in field_info["properties"]["valid_types"]["value"] for sub_item in item): raise ShotgunError("%s.%s is of multi-type entity, but an item in data %s has an invalid type (expected one of %s)" % (entity_type, field, item, field_info["properties"]["valid_types"]["value"])) - - + + elif field_info["data_type"]["value"] == "entity": if not isinstance(item, dict): raise ShotgunError("%s.%s is of type entity, but data %s is not a dictionary" % (entity_type, field, item)) @@ -468,8 +528,8 @@ def _validate_entity_data(self, entity_type, data): "status_list": basestring, "url": dict}[sg_type] except KeyError: - raise ShotgunError("Field %s.%s: Handling for Shotgun type %s is not implemented" % (entity_type, field, sg_type)) - + raise ShotgunError("Field %s.%s: Handling for Shotgun type %s is not implemented" % (entity_type, field, sg_type)) + if not isinstance(item, python_type): raise ShotgunError("%s.%s is of type %s, but data %s is not of type %s" % (entity_type, field, type(item), sg_type, python_type)) @@ -561,6 +621,11 @@ def _compare(self, field_type, lval, operator, rval): elif operator == "is_not": return lval != rval elif operator == "in": +<<<<<<< HEAD:shotgun_api3/lib/mockgun/mockgun.py +======= + return lval in rval + elif operator == "contains": +>>>>>>> origin/master:shotgun_api3/lib/mockgun.py return lval in rval elif operator == "contains": return rval in lval @@ -620,6 +685,7 @@ def _get_field_from_row(self, entity_type, row, field): field_value = row[field2] +<<<<<<< HEAD:shotgun_api3/lib/mockgun/mockgun.py # If we have a list of links, retrieve the subfields one by one. if isinstance(field_value, list): values = [] @@ -639,6 +705,10 @@ def _get_field_from_row(self, entity_type, row, field): return values # not multi entity, must be entity. elif not isinstance(field_value, dict): +======= + # all deep links need to be link fields + if not isinstance(field_value, dict): +>>>>>>> origin/master:shotgun_api3/lib/mockgun.py raise ShotgunError("Invalid deep query field %s.%s" % (entity_type, field)) # make sure that types in the query match type in the linked field @@ -671,12 +741,18 @@ def _get_field_type(self, entity_type, field): except ValueError: return self._schema[entity_type][field]["data_type"]["value"] +<<<<<<< HEAD:shotgun_api3/lib/mockgun/mockgun.py def _row_matches_filter(self, entity_type, row, sg_filter, retired_only): +======= + def _row_matches_filter(self, entity_type, row, filter): + +>>>>>>> origin/master:shotgun_api3/lib/mockgun.py try: field, operator, rval = sg_filter except ValueError: raise ShotgunError("Filters must be in the form [lval, operator, rval]") +<<<<<<< HEAD:shotgun_api3/lib/mockgun/mockgun.py # Special case, field is None when we have a filter operator. if field is None: @@ -753,6 +829,22 @@ def _rearrange_filters(self, filters): def _row_matches_filters(self, entity_type, row, filters, filter_operator, retired_only): filters = self._rearrange_filters(filters) +======= + lval = self._get_field_from_row(entity_type, row, field) + + field_type = self._get_field_type(entity_type, field) + + # if we're operating on an entity, we'll need to grab the name from the lval's row + if field_type == "entity": + lval_row = self._db[lval["type"]][lval["id"]] + if "name" in lval_row: + lval["name"] = lval_row["name"] + elif "code" in lval_row: + lval["name"] = lval_row["code"] + return self._compare(field_type, lval, operator, rval) + + def _row_matches_filters(self, entity_type, row, filters, filter_operator, retired_only): +>>>>>>> origin/master:shotgun_api3/lib/mockgun.py if retired_only and not row["__retired"] or not retired_only and row["__retired"]: # ignore retired rows unless the retired_only flag is set @@ -775,7 +867,7 @@ def _update_row(self, entity_type, row, data): row[field] = [{"type": item["type"], "id": item["id"]} for item in data[field]] else: row[field] = data[field] - + def _validate_entity_exists(self, entity_type, entity_id): if entity_id not in self._db[entity_type]: diff --git a/shotgun_api3/shotgun.py b/shotgun_api3/shotgun.py index 9f9b18efa..a8714b8c8 100755 --- a/shotgun_api3/shotgun.py +++ b/shotgun_api3/shotgun.py @@ -81,7 +81,7 @@ not require the added security provided by enforcing this. """ try: - import ssl + import ssl except ImportError, e: if "SHOTGUN_FORCE_CERTIFICATE_VALIDATION" in os.environ: raise ImportError("%s. SHOTGUN_FORCE_CERTIFICATE_VALIDATION environment variable prevents " @@ -136,8 +136,8 @@ class ServerCapabilities(object): .. warning:: - This class is part of the internal API and its interfaces may change at any time in - the future. Therefore, usage of this class is discouraged. + This class is part of the internal API and its interfaces may change at any time in + the future. Therefore, usage of this class is discouraged. """ def __init__(self, host, meta): @@ -274,8 +274,8 @@ class ClientCapabilities(object): .. warning:: - This class is part of the internal API and its interfaces may change at any time in - the future. Therefore, usage of this class is discouraged. + This class is part of the internal API and its interfaces may change at any time in + the future. Therefore, usage of this class is discouraged. :ivar str platform: The current client platform. Valid values are ``mac``, ``linux``, ``windows``, or ``None`` (if the current platform couldn't be determined). @@ -327,8 +327,8 @@ class _Config(object): def __init__(self): self.max_rpc_attempts = 3 # From http://docs.python.org/2.6/library/httplib.html: - # If the optional timeout parameter is given, blocking operations - # (like connection attempts) will timeout after that many seconds + # If the optional timeout parameter is given, blocking operations + # (like connection attempts) will timeout after that many seconds # (if it is not given, the global default timeout setting is used) self.timeout_secs = None self.api_ver = 'api3' @@ -347,9 +347,9 @@ def __init__(self): self.scheme = None self.server = None self.api_path = None - # The raw_http_proxy reflects the exact string passed in - # to the Shotgun constructor. This can be useful if you - # need to construct a Shotgun API instance based on + # The raw_http_proxy reflects the exact string passed in + # to the Shotgun constructor. This can be useful if you + # need to construct a Shotgun API instance based on # another Shotgun API instance. self.raw_http_proxy = None # if a proxy server is being used, the proxy_handler @@ -484,7 +484,7 @@ def __init__(self, if login is not None or password is not None: 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 " @@ -581,7 +581,7 @@ def __init__(self, self._json_loads = self._json_loads_ascii self.client_caps = ClientCapabilities() - # this relies on self.client_caps being set first + # this relies on self.client_caps being set first self.reset_user_agent() self._server_caps = None @@ -629,7 +629,7 @@ def server_caps(self): Property containing :class:`ServerCapabilities` object. >>> sg.server_caps - + :returns: :class:`ServerCapabilities` object that describe the server the client is connected to. @@ -1604,12 +1604,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,)) - + params = dict( user=user, entity=entity ) - + return self._call_rpc('follow', params) def unfollow(self, user, entity): @@ -1632,12 +1632,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,)) - + params = dict( user=user, entity=entity ) - + return self._call_rpc('unfollow', params) def followers(self, entity): @@ -1661,11 +1661,11 @@ 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,)) - + params = dict( entity=entity ) - + return self._call_rpc('followers', params) def following(self, user, project=None, entity_type=None): @@ -1980,20 +1980,20 @@ def reset_user_agent(self): Example default user-agent:: - shotgun-json (3.0.17); Python 2.6 (Mac); ssl OpenSSL 1.0.2d 9 Jul 2015 (validate) + shotgun_repos-json (3.0.17); Python 2.6 (Mac); ssl OpenSSL 1.0.2d 9 Jul 2015 (validate) """ ua_platform = "Unknown" if self.client_caps.platform is not None: ua_platform = self.client_caps.platform.capitalize() - + # create ssl validation string based on settings validation_str = "validate" if self.config.no_ssl_validation: validation_str = "no-validate" - - self._user_agents = ["shotgun-json (%s)" % __version__, + + self._user_agents = ["shotgun_repos-json (%s)" % __version__, "Python %s (%s)" % (self.client_caps.py_version, ua_platform), "ssl %s (%s)" % (self.client_caps.ssl_version, validation_str)] @@ -2452,7 +2452,7 @@ def download_attachment(self, attachment=False, file_path=None, attachment_id=No ``file_path`` is ``None``, returns the actual data of the file as a string. :rtype: str """ - # backwards compatibility when passed via keyword argument + # backwards compatibility when passed via keyword argument if attachment is False: if type(attachment_id) == int: attachment = attachment_id @@ -2466,7 +2466,7 @@ def download_attachment(self, attachment=False, file_path=None, attachment_id=No fp = open(file_path, 'wb') except IOError, e: raise IOError("Unable to write Attachment to disk using "\ - "file_path. %s" % e) + "file_path. %s" % e) url = self.get_attachment_download_url(attachment) if url is None: @@ -2475,7 +2475,7 @@ def download_attachment(self, attachment=False, file_path=None, attachment_id=No # We only need to set the auth cookie for downloads from Shotgun server if self.config.server in url: self.set_up_auth_cookie() - + try: request = urllib2.Request(url) request.add_header('user-agent', "; ".join(self._user_agents)) @@ -2494,7 +2494,7 @@ def download_attachment(self, attachment=False, file_path=None, attachment_id=No if e.code == 400: 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. + # 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': body = e.readlines() @@ -2561,7 +2561,7 @@ def get_attachment_download_url(self, attachment): try: url = attachment['url'] except KeyError: - if ('id' in attachment and 'type' in attachment and + if ('id' in attachment and 'type' in attachment and attachment['type'] == 'Attachment'): attachment_id = attachment['id'] else: @@ -2572,7 +2572,7 @@ def get_attachment_download_url(self, attachment): raise TypeError("Unable to determine download url. Expected "\ "dict, int, or NoneType. Instead got %s" % type(attachment)) - if attachment_id: + if attachment_id: url = urlparse.urlunparse((self.config.scheme, self.config.server, "/file_serve/attachment/%s" % urllib.quote(str(attachment_id)), None, None, None)) @@ -2736,10 +2736,10 @@ def note_thread_read(self, note_id, entity_fields=None): "higher, server is %s" % (self.server_caps.version,)) entity_fields = entity_fields or {} - + if not isinstance(entity_fields, dict): raise ValueError("entity_fields parameter must be a dictionary") - + params = { "note_id": note_id, "entity_fields": entity_fields } record = self._call_rpc("note_thread_contents", params) @@ -2806,25 +2806,25 @@ def text_search(self, text, entity_types, project_ids=None, limit=None): 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,)) - - # convert entity_types structure into the form + + # convert entity_types structure into the form # that the API endpoint expects if not isinstance(entity_types, dict): raise ValueError("entity_types parameter must be a dictionary") - + api_entity_types = {} for (entity_type, filter_list) in entity_types.iteritems(): if isinstance(filter_list, (list, tuple)): resolved_filters = _translate_filters(filter_list, filter_operator=None) - api_entity_types[entity_type] = resolved_filters + api_entity_types[entity_type] = resolved_filters else: raise ValueError("value of entity_types['%s'] must " "be a list or tuple." % entity_type) - + project_ids = project_ids or [] - params = { "text": text, + params = { "text": text, "entity_types": api_entity_types, "project_ids": project_ids, "max_results": limit } @@ -2906,10 +2906,10 @@ def activity_stream_read(self, entity_type, entity_id, entity_fields=None, min_i # set up parameters to send to server. entity_fields = entity_fields or {} - + 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, @@ -3022,9 +3022,9 @@ 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): @@ -3113,8 +3113,8 @@ def _auth_params(self): auth_params = {"session_token" : str(self.config.session_token)} - # Request server side to raise exception for expired sessions. - # This was added in as part of Shotgun 5.4.4 + # Request server side to raise exception for expired sessions. + # This was added in as part of Shotgun 5.4.4 if self.server_caps.version and self.server_caps.version > (5, 4, 3): auth_params["reject_if_expired"] = True @@ -3206,24 +3206,24 @@ def _make_call(self, verb, path, body, headers): return self._http_request(verb, path, body, req_headers) except SSLHandshakeError, e: # Test whether the exception is due to the fact that this is an older version of - # Python that cannot validate certificates encrypted with SHA-2. If it is, then + # Python that cannot validate certificates encrypted with SHA-2. If it is, then # fall back on disabling the certificate validation and try again - unless the - # SHOTGUN_FORCE_CERTIFICATE_VALIDATION environment variable has been set by the - # user. In that case we simply raise the exception. Any other exceptions simply - # get raised as well. + # SHOTGUN_FORCE_CERTIFICATE_VALIDATION environment variable has been set by the + # user. In that case we simply raise the exception. Any other exceptions simply + # get raised as well. # # For more info see: # http://blog.shotgunsoftware.com/2016/01/important-ssl-certificate-renewal-and.html # - # SHA-2 errors look like this: + # SHA-2 errors look like this: # [Errno 1] _ssl.c:480: error:0D0C50A1:asn1 encoding routines:ASN1_item_verify: # unknown message digest algorithm - # + # # Any other exceptions simply get raised. if not str(e).endswith("unknown message digest algorithm") or \ "SHOTGUN_FORCE_CERTIFICATE_VALIDATION" in os.environ: raise - + if self.config.no_ssl_validation is False: LOG.warning("SSLHandshakeError: this Python installation is incompatible with " "certificates signed with SHA-2. Disabling certificate validation. " @@ -3232,7 +3232,7 @@ def _make_call(self, verb, path, body, headers): 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) - + self._close_connection() if attempt == max_rpc_attempts: raise @@ -3359,7 +3359,7 @@ def _response_errors(self, sg_response): elif sg_response.get("error_code") == ERR_2FA: raise MissingTwoFactorAuthenticationFault(sg_response.get("message", "Unknown 2FA Authentication Error")) else: - # raise general Fault + # raise general Fault raise Fault(sg_response.get("message", "Unknown Error")) return @@ -3536,7 +3536,7 @@ def _parse_records(self, records): if isinstance(v, types.StringTypes): rec[k] = rec[k].replace('<', '<') - # check for thumbnail for older version (<3.3.0) of shotgun + # check for thumbnail for older version (<3.3.0) of shotgun_repos if k == 'image' and \ self.server_caps.version and \ self.server_caps.version < (3, 3, 0): @@ -3851,7 +3851,7 @@ def _translate_filters(filters, filter_operator): def _translate_filters_dict(sg_filter): new_filters = {} filter_operator = sg_filter.get("filter_operator") - + if filter_operator == "all" or filter_operator == "and": new_filters["logical_operator"] = "and" elif filter_operator == "any" or filter_operator == "or": @@ -3864,12 +3864,12 @@ def _translate_filters_dict(sg_filter): % sg_filter["filters"]) new_filters["conditions"] = _translate_filters_list(sg_filter["filters"]) - + return new_filters - + def _translate_filters_list(filters): conditions = [] - + for sg_filter in filters: if isinstance(sg_filter, (list,tuple)): conditions.append(_translate_filters_simple(sg_filter)) @@ -3886,7 +3886,7 @@ def _translate_filters_simple(sg_filter): "path": sg_filter[0], "relation": sg_filter[1] } - + values = sg_filter[2:] if len(values) == 1 and isinstance(values[0], (list, tuple)): values = values[0] diff --git a/tests/test_api.py b/tests/test_api.py index b76cb1717..bbbea64e4 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1589,7 +1589,7 @@ def test_bad_auth(self): sg = shotgun_api3.Shotgun(server_url, login=login, password='not a real password') self.assertRaises(shotgun_api3.AuthenticationFault, sg.find_one, 'Shot',[]) - @patch('shotgun_api3.shotgun.Http.request') + @patch('shotgun_api3.shotgun_repos.Http.request') def test_status_not_200(self, mock_request): response = MagicMock(name="response mock", spec=dict) response.status = 300 @@ -1597,7 +1597,7 @@ def test_status_not_200(self, mock_request): mock_request.return_value = (response, {}) self.assertRaises(shotgun_api3.ProtocolError, self.sg.find_one, 'Shot', []) - @patch('shotgun_api3.shotgun.Http.request') + @patch('shotgun_api3.shotgun_repos.Http.request') def test_sha2_error(self, mock_request): # Simulate the SSLHandshakeError raised with SHA-2 errors mock_request.side_effect = SSLHandshakeError("[Errno 1] _ssl.c:480: error:0D0C50A1:asn1 " @@ -1635,7 +1635,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_repos.Http.request') def test_sha2_error_with_strict(self, mock_request): # Simulate the SSLHandshakeError raised with SHA-2 errors mock_request.side_effect = SSLHandshakeError("[Errno 1] _ssl.c:480: error:0D0C50A1:asn1 " @@ -1655,7 +1655,7 @@ def test_sha2_error_with_strict(self, mock_request): result = self.sg.info() except SSLHandshakeError: # ensure the api has NOT reset the values in the fallback behavior because we have - # set the env variable to force validation + # set the farmjob variable to force validation self.assertFalse(self.sg.config.no_ssl_validation) self.assertFalse(shotgun_api3.shotgun.NO_SSL_VALIDATION) self.assertFalse("(no-validate)" in " ".join(self.sg._user_agents)) @@ -1691,7 +1691,7 @@ def test_upload_empty_file(self): 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', + self.assertRaises(shotgun_api3.ShotgunError, self.sg.upload_filmstrip_thumbnail, 'Version', 123, path) def test_upload_missing_file(self): @@ -1701,7 +1701,7 @@ def test_upload_missing_file(self): 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', + self.assertRaises(shotgun_api3.ShotgunError, self.sg.upload_filmstrip_thumbnail, 'Version', 123, path) # def test_malformed_response(self): @@ -2111,7 +2111,7 @@ def _check_reply(self, data, reply_id, additional_fields): # the reply stream adds an image to the user fields in order # to include thumbnails for users, so remove this before we compare - # against the shotgun find data. The image is tested elsewhere. + # against the shotgun_repos find data. The image is tested elsewhere. del data["user"]["image"] self.assertEqual(reply_data, data) @@ -2515,7 +2515,7 @@ def _has_unicode(data): def _get_path(url): """Returns path component of a url without the sheme, host, query, anchor, or any other - additional elements. + additional elements. For example, the url "https://foo.shotgunstudio.com/page/2128#Shot_1190_sr10101_034" returns "/page/2128" """ diff --git a/tests/test_api_long.py b/tests/test_api_long.py index a5fb21f85..5d1b35939 100644 --- a/tests/test_api_long.py +++ b/tests/test_api_long.py @@ -28,13 +28,13 @@ def test_automated_find(self): continue # trying to use some different code paths to the other find test - # pivot_column fields aren't valid for sorting so ensure we're + # pivot_column fields aren't valid for sorting so ensure we're # not using one. order_field = None for field_name, field in fields.iteritems(): if field['data_type']["value"] != 'pivot_column': order_field = field_name - break + break # TODO for our test project, we haven't populated these entities.... order = [{'field_name': order_field, 'direction': direction}] if "project" in fields: @@ -79,20 +79,20 @@ def test_schema(self): self.assertTrue(len(schema) > 0) self.assertTrue("user" in schema) - # An explanation is in order here. the field code that is created in shotgun is based on the human display name + # An explanation is in order here. the field code that is created in shotgun_repos is based on the human display name # that is provided , so for example "Money Count" would generate the field code 'sg_monkey_count' . The field - # that is created in this test is retired at the end of the test but when this test is run again against + # that is created in this test is retired at the end of the test but when this test is run again against # the same database ( which happens on our Continuous Integration server ) trying to create a new field - # called "Monkey Count" will now fail due to the new Delete Field Forever features we have added to shotgun + # called "Monkey Count" will now fail due to the new Delete Field Forever features we have added to shotgun_repos # since there will a retired field called sg_monkey_count. The old behavior was to go ahead and create a new # "Monkey Count" field with a field code with an incremented number of the end like sg_monkey_count_1. The new # behavior is to raise an error in hopes the user will go into the UI and delete the old retired field forever. # make a the name of the field somewhat unique human_field_name = "Monkey " + str(random.getrandbits(24)) - + properties = { "description" : "How many monkeys were needed" } - new_field_name = self.sg.schema_field_create("Version", "number", human_field_name, + new_field_name = self.sg.schema_field_create("Version", "number", human_field_name, properties=properties) properties = {"description": "How many monkeys turned up"} diff --git a/tests/test_client.py b/tests/test_client.py index e3e85d401..34d5d0b2a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -25,7 +25,7 @@ import base class TestShotgunClient(base.MockTestBase): - '''Test case for shotgun api with server interactions mocked.''' + '''Test case for shotgun_repos api with server interactions mocked.''' def setUp(self): super(TestShotgunClient, self).setUp() @@ -184,8 +184,8 @@ def test_user_agent(self): args, _ = self.sg._http_request.call_args (_, _, _, headers) = args ssl_validate_lut = {True: "no-validate", False: "validate"} - expected = "shotgun-json (%s); Python %s (%s); ssl %s (%s)" % ( - api.__version__, + expected = "shotgun_repos-json (%s); Python %s (%s); ssl %s (%s)" % ( + api.__version__, client_caps.py_version, client_caps.platform.capitalize(), client_caps.ssl_version, @@ -198,8 +198,8 @@ def test_user_agent(self): self.sg.info() args, _ = self.sg._http_request.call_args (_, _, _, headers) = args - expected = "shotgun-json (%s); Python %s (%s); ssl %s (%s); test-agent" % ( - api.__version__, + expected = "shotgun_repos-json (%s); Python %s (%s); ssl %s (%s); test-agent" % ( + api.__version__, client_caps.py_version, client_caps.platform.capitalize(), client_caps.ssl_version, @@ -212,8 +212,8 @@ def test_user_agent(self): self.sg.info() args, _ = self.sg._http_request.call_args (_, _, _, headers) = args - expected = "shotgun-json (%s); Python %s (%s); ssl %s (%s)" % ( - api.__version__, + expected = "shotgun_repos-json (%s); Python %s (%s); ssl %s (%s)" % ( + api.__version__, client_caps.py_version, client_caps.platform.capitalize(), client_caps.ssl_version, @@ -477,7 +477,7 @@ def test_thumb_url(self): "FakeAsset", 456) class TestShotgunClientInterface(base.MockTestBase): - '''Tests expected interface for shotgun module and client''' + '''Tests expected interface for shotgun_repos module and client''' def test_client_interface(self): expected_attributes = ['base_url', 'config', diff --git a/tests/tests_unit.py b/tests/tests_unit.py index 2f2a04db2..1356ae216 100755 --- a/tests/tests_unit.py +++ b/tests/tests_unit.py @@ -15,18 +15,18 @@ def setUp(self): 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, + sg = api.Shotgun(self.server_path, + self.script_name, + self.api_key, http_proxy=http_proxy, connect=False) self.assertEquals(sg.config.proxy_server, proxy_server) self.assertEquals(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, + sg = api.Shotgun(self.server_path, + self.script_name, + self.api_key, http_proxy=http_proxy, connect=False) self.assertEquals(sg.config.proxy_server, proxy_server) @@ -36,9 +36,9 @@ 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, + sg = api.Shotgun(self.server_path, + self.script_name, + self.api_key, http_proxy=http_proxy, connect=False) self.assertEquals(sg.config.proxy_server, proxy_server) @@ -46,9 +46,9 @@ def test_http_proxy_server_and_port(self): 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, + sg = api.Shotgun(self.server_path, + self.script_name, + self.api_key, http_proxy=http_proxy, connect=False) self.assertEquals(sg.config.proxy_server, proxy_server) @@ -59,11 +59,11 @@ 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, + 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, + sg = api.Shotgun(self.server_path, + self.script_name, + self.api_key, http_proxy=http_proxy, connect=False) self.assertEquals(sg.config.proxy_server, proxy_server) @@ -74,11 +74,11 @@ 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, + 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, + sg = api.Shotgun(self.server_path, + self.script_name, + self.api_key, http_proxy=http_proxy, connect=False) self.assertEquals(sg.config.proxy_server, proxy_server) @@ -91,29 +91,29 @@ 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, + 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, + sg = api.Shotgun(self.server_path, + self.script_name, + self.api_key, http_proxy=http_proxy, connect=False) self.assertEquals(sg.config.proxy_server, proxy_server) self.assertEquals(sg.config.proxy_port, proxy_port) self.assertEquals(sg.config.proxy_user, proxy_user) self.assertEquals(sg.config.proxy_pass, proxy_pass) - + def test_malformatted_proxy_info(self): proxy_server = "someserver.com" proxy_port = 1234 proxy_user = "user" proxy_pass = "password" - http_proxy = "%s:%s@%s:%d" % (proxy_user, proxy_pass, proxy_server, + http_proxy = "%s:%s@%s:%d" % (proxy_user, proxy_pass, proxy_server, proxy_port) conn_info = { 'base_url': self.server_path, 'script_name': self.script_name, - 'api_key': self.api_key, + 'api_key': self.api_key, 'connect': False, } conn_info['http_proxy'] = 'http://someserver.com' @@ -122,7 +122,7 @@ def test_malformatted_proxy_info(self): self.assertRaises(ValueError, api.Shotgun, **conn_info) 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 validation as it exists in Shotgun.summarize. @@ -130,8 +130,8 @@ class TestShotgunSummarize(unittest.TestCase): Does not require database connection or test data.''' def setUp(self): self.sg = api.Shotgun('http://server_path', - 'script_name', - 'api_key', + 'script_name', + 'api_key', connect=False) @@ -164,7 +164,7 @@ def test_filters(self): result = self.get_call_rpc_params(args, {}) actual_condition = result['filters']['conditions'][0] self.assertEquals(expected_condition, actual_condition) - + @patch('shotgun_api3.Shotgun._call_rpc') def get_call_rpc_params(self, args, kws, call_rpc): '''Return params sent to _call_rpc from summarize.''' @@ -177,7 +177,7 @@ def test_grouping(self): result = self.get_call_rpc_params(None, {}) self.assertFalse(result.has_key('grouping')) grouping = ['something'] - kws = {'grouping':grouping} + kws = {'grouping':grouping} result = self.get_call_rpc_params(None, kws) self.assertEqual(grouping, result['grouping']) @@ -188,8 +188,8 @@ def test_grouping_type(self): class TestShotgunBatch(unittest.TestCase): def setUp(self): self.sg = api.Shotgun('http://server_path', - 'script_name', - 'api_key', + 'script_name', + 'api_key', connect=False) def test_missing_required_key(self): @@ -241,7 +241,7 @@ def test_darwin(self): def test_windows(self): self.assert_platform('win32','windows') - + def test_linux(self): self.assert_platform('Linux', 'linux') @@ -266,8 +266,8 @@ def test_no_platform(self): self.assertEquals(client_caps.local_path_field, None) finally: api.shotgun.sys.platform = platform - - @patch('shotgun_api3.shotgun.sys') + + @patch('shotgun_api3.shotgun_repos.sys') def test_py_version(self, mock_sys): major = 2 minor = 7 @@ -276,17 +276,17 @@ def test_py_version(self, mock_sys): expected_py_version = "%s.%s" % (major, minor) client_caps = api.shotgun.ClientCapabilities() self.assertEquals(client_caps.py_version, expected_py_version) - + class TestFilters(unittest.TestCase): def test_empty(self): expected = { "logical_operator": "and", "conditions": [] } - + result = api.shotgun._translate_filters([], None) self.assertEquals(result, expected) - + def test_simple(self): filters = [ ["code", "is", "test"], @@ -300,10 +300,10 @@ def test_simple(self): { "path": "sg_status_list", "relation": "is", "values": ["ip"] } ] } - + result = api.shotgun._translate_filters(filters, "any") self.assertEquals(result, expected) - + # Test both styles of passing arrays def test_arrays(self): expected = { @@ -312,18 +312,18 @@ def test_arrays(self): { "path": "code", "relation": "in", "values": ["test1", "test2", "test3"] } ] } - + filters = [ ["code", "in", "test1", "test2", "test3"] ] - + result = api.shotgun._translate_filters(filters, "all") self.assertEquals(result, expected) - + filters = [ ["code", "in", ["test1", "test2", "test3"]] ] - + result = api.shotgun._translate_filters(filters, "all") self.assertEquals(result, expected) @@ -345,7 +345,7 @@ def test_nested(self): ] } ] - + expected = { "logical_operator": "and", "conditions": [ @@ -366,32 +366,32 @@ def test_nested(self): } ] } - + result = api.shotgun._translate_filters(filters, "all") self.assertEquals(result, expected) - + def test_invalid(self): self.assertRaises(api.ShotgunError, api.shotgun._translate_filters, [], "bogus") self.assertRaises(api.ShotgunError, api.shotgun._translate_filters, ["bogus"], "all") - + filters = [{ "filter_operator": "bogus", "filters": [] }] - + self.assertRaises(api.ShotgunError, api.shotgun._translate_filters, filters, "all") - + filters = [{ "filters": [] }] - + self.assertRaises(api.ShotgunError, api.shotgun._translate_filters, filters, "all") - + filters = [{ "filter_operator": "all", "filters": { "bogus": "bogus" } }] - + self.assertRaises(api.ShotgunError, api.shotgun._translate_filters, filters, "all")