Skip to content

Commit b85a3e3

Browse files
authored
Merge branch 'master' into feature/ah/mockgun_set_schema
2 parents 6a6e973 + 3a80f5b commit b85a3e3

File tree

5 files changed

+243
-0
lines changed

5 files changed

+243
-0
lines changed

shotgun_api3/lib/mockgun/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@
3131
"""
3232

3333
from .schema import generate_schema # noqa
34+
from .data import generate_data # noqa
3435
from .mockgun import Shotgun # noqa
3536
from .errors import MockgunError # noqa

shotgun_api3/lib/mockgun/data.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
"""
2+
-----------------------------------------------------------------------------
3+
Copyright (c) 2009-2019, Shotgun Software Inc
4+
5+
Redistribution and use in source and binary forms, with or without
6+
modification, are permitted provided that the following conditions are met:
7+
8+
- Redistributions of source code must retain the above copyright notice, this
9+
list of conditions and the following disclaimer.
10+
11+
- Redistributions in binary form must reproduce the above copyright notice,
12+
this list of conditions and the following disclaimer in the documentation
13+
and/or other materials provided with the distribution.
14+
15+
- Neither the name of the Shotgun Software Inc nor the names of its
16+
contributors may be used to endorse or promote products derived from this
17+
software without specific prior written permission.
18+
19+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
30+
-----------------------------------------------------------------------------
31+
"""
32+
# TODO: Python3 support
33+
# TODO: Logging not printing
34+
import cPickle as pickle
35+
import os
36+
37+
from .errors import MockgunError
38+
from .schema import SchemaFactory
39+
40+
# Highest protocol that Python 2.4 supports, which is the earliest version of Python we support.
41+
# Actually, this is the same version that Python 2.7 supports at the moment!
42+
_HIGHEST_24_PICKLE_PROTOCOL = 2
43+
44+
45+
# Global private values to cache the schema in.
46+
__schema, __schema_entity = None, None
47+
48+
49+
def _get_schema(force=False):
50+
"""
51+
_get_schema will get the schema from the SchemaFactory and cache it.
52+
53+
:param bool force: If set, force will always query the latest schema from disk.
54+
:return: schema dictionary from disk
55+
"""
56+
global __schema, __schema_entity
57+
from .mockgun import Shotgun
58+
if not __schema or force is True:
59+
schema_path, schema_entity_path = Shotgun.get_schema_paths()
60+
if not schema_path or not schema_entity_path:
61+
raise MockgunError("You must set the schema paths on the Mockgun instance first.")
62+
__schema, __schema_entity = SchemaFactory.get_schemas(schema_path, schema_entity_path)
63+
return __schema
64+
65+
66+
def _get_schema_entity(force=False):
67+
"""
68+
_get_schema_entity will get the schema_entity from the SchemaFactory and cache it.
69+
70+
:param bool force: If set, force will always query the latest schema_entity from disk.
71+
:return: schema_entity dictionary from disk
72+
"""
73+
global __schema, __schema_entity
74+
from .mockgun import Shotgun
75+
if not __schema_entity or force is True:
76+
schema_path, schema_entity_path = Shotgun.get_schema_paths()
77+
if not schema_path or not schema_entity_path:
78+
raise MockgunError("You must set the schema paths on the Mockgun instance first.")
79+
__schema, __schema_entity = SchemaFactory.get_schemas(schema_path, schema_entity_path)
80+
return __schema_entity
81+
82+
83+
def _get_entity_fields(entity):
84+
"""
85+
_get_entity_fields will return a list of the fields on an entity as strings
86+
:param str entity: Shotgun entity that we want the schema for
87+
:return: List of the field names for the provided entity
88+
:rtype: list[str]
89+
"""
90+
schema = _get_schema()
91+
return schema[entity].keys()
92+
93+
94+
def _read_data_(shotgun, entity):
95+
"""
96+
_read_data_ will return all of the entries for the provided entity.
97+
It will get all fields for the entity from the Mockgun schema.
98+
99+
:param shotgun: Shotgun instance used to query a live site
100+
:param str entity: Shotgun entity that we want the schema for
101+
:return: List of found entities
102+
:rtype: list[dict]
103+
"""
104+
try:
105+
return shotgun.find(
106+
entity,
107+
filters=[],
108+
fields=_get_entity_fields(entity)
109+
)
110+
except Exception as err:
111+
print(" Exception: %s" % str(err))
112+
import traceback
113+
traceback.print_exc()
114+
return []
115+
116+
117+
class DatabaseFactory(object):
118+
"""
119+
Allows to instantiate a pickled database.
120+
"""
121+
_database_cache = None
122+
_database_cache_path = None
123+
124+
@classmethod
125+
def get_database(cls, database_path):
126+
"""
127+
Retrieves the schemas from disk.
128+
129+
:param str database_path: Path to the database.
130+
131+
:returns: Dictionary holding the database.
132+
:rtype: dict
133+
"""
134+
if not os.path.exists(database_path):
135+
raise MockgunError("Cannot locate Mockgun database file '%s'!" % database_path)
136+
137+
# Poor man's attempt at a cache. All of our use cases deal with a single pair of files
138+
# for the duration of the unit tests, so keep a cache for both inputs. We don't want
139+
# to deal with ever growing caches anyway. Just having this simple cache has shown
140+
# speed increases of up to 500% for Toolkit unit tests alone.
141+
142+
if database_path != cls._database_cache_path:
143+
cls._database_cache = cls._read_file(database_path)
144+
cls._database_cache_path = database_path
145+
146+
return cls._database_cache
147+
148+
@classmethod
149+
def _read_file(cls, path):
150+
fh = open(path, "rb")
151+
try:
152+
return pickle.load(fh)
153+
finally:
154+
fh.close()
155+
156+
@classmethod
157+
def _write_file(cls, data, path):
158+
fh = open(path, "rb")
159+
try:
160+
return pickle.dump(data, fh, protocol=_HIGHEST_24_PICKLE_PROTOCOL)
161+
finally:
162+
fh.close()
163+
164+
@classmethod
165+
def set_database(cls, database, database_path):
166+
"""
167+
Writes the schemas to disk.
168+
169+
:param dict database: The database in memory.
170+
:param str database_path: Path to the database.
171+
"""
172+
if database_path != cls._database_cache_path:
173+
cls._database_cache_path = database_path
174+
cls._database_cache = database
175+
176+
cls._write_file(database, database_path)
177+
178+
179+
# ----------------------------------------------------------------------------
180+
# Utility methods
181+
def generate_data(shotgun, data_file_path, entity_subset=None):
182+
"""
183+
Helper method for mockgun.
184+
Generates the data files needed by the mocker by connecting to a real shotgun
185+
and downloading the information for that site. Once the generated data
186+
files are being passed to mockgun, it will mimic the site's database structure.
187+
188+
:param shotgun: Shotgun instance
189+
:param data_file_path: Path where to write the main data file to
190+
:param entity_subset: Optional subset of entities to generate data for.
191+
If not passed, it will default to all entities
192+
"""
193+
194+
if not entity_subset:
195+
entity_subset = _get_schema().keys()
196+
197+
database = {}
198+
for entity in entity_subset:
199+
print("Requesting data for: %s" % entity)
200+
database[entity] = _read_data_(shotgun, entity)
201+
202+
DatabaseFactory.set_database(database, data_file_path)

shotgun_api3/lib/mockgun/mockgun.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
from ...shotgun import _Config
122122
from .errors import MockgunError
123123
from .schema import SchemaFactory
124+
from .data import DatabaseFactory
124125

125126
# ----------------------------------------------------------------------------
126127
# Version
@@ -148,6 +149,19 @@ class Shotgun(object):
148149

149150
__schema_path = None
150151
__schema_entity_path = None
152+
__database_path = None
153+
154+
@classmethod
155+
def set_database_path(cls, database_path):
156+
"""
157+
Set the path where the database can be found. This is done at the class
158+
level so all Shotgun instances will share the same database.
159+
The responsibility to generate and load these files is left to the user
160+
changing the default value.
161+
162+
:param database_path: Directory path where the database is.
163+
"""
164+
cls.__database_path = database_path
151165

152166
@classmethod
153167
def set_schema_paths(cls, schema_path, schema_entity_path):
@@ -162,6 +176,16 @@ def set_schema_paths(cls, schema_path, schema_entity_path):
162176
cls.__schema_path = schema_path
163177
cls.__schema_entity_path = schema_entity_path
164178

179+
@classmethod
180+
def get_database_path(cls):
181+
"""
182+
Returns the file which are part of the database.
183+
These path can then be used in generate_database if needed.
184+
185+
:returns: A string with database_path
186+
"""
187+
return cls.__database_path
188+
165189
@classmethod
166190
def get_schema_paths(cls):
167191
"""
@@ -205,6 +229,7 @@ def __init__(self,
205229

206230
# initialize the "database"
207231
self._db = dict((entity, {}) for entity in self._schema)
232+
self._update_db()
208233

209234
# set some basic public members that exist in the Shotgun API
210235
self.base_url = base_url
@@ -462,6 +487,9 @@ def dump_schemas(self):
462487
schema_path, schema_entity_path = self.get_schema_paths()
463488
SchemaFactory.set_schemas(self.schema_read(), schema_path, self.schema_entity_read(), schema_entity_path)
464489

490+
def dump_database(self):
491+
DatabaseFactory.set_database(self._db, self.get_database_path())
492+
465493
###################################################################################################
466494
# internal methods and members
467495
@classmethod
@@ -482,6 +510,14 @@ def _set_property(cls, property_data, property_key, property_value):
482510
result = True
483511
return result
484512

513+
def _update_db(self):
514+
database = DatabaseFactory.get_database(self.get_database_path())
515+
for entity_type in database:
516+
for entity in database[entity_type]:
517+
row = self._get_new_row(entity_type)
518+
self._update_row(entity_type, row, entity)
519+
self._db[entity_type][row["id"]] = row
520+
485521
def _validate_entity_type(self, entity_type):
486522
if entity_type not in self._schema:
487523
raise ShotgunError("%s is not a valid entity" % entity_type)

tests/mockgun/database.pickle

14.1 KB
Binary file not shown.

tests/test_mockgun.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
os.path.join(mockgun_schema_folder, "schema.pickle"),
5252
os.path.join(mockgun_schema_folder, "schema_entity.pickle")
5353
)
54+
Mockgun.set_database_path(
55+
os.path.join(mockgun_schema_folder, "database.pickle"),
56+
)
5457

5558

5659
# FIXME: This should probably be refactored into a base class for
@@ -90,6 +93,7 @@ def test_interface_intact(self):
9093
# error.
9194
mockgun.MockgunError
9295
mockgun.generate_schema
96+
mockgun.generate_data
9397
mockgun.Shotgun
9498

9599

0 commit comments

Comments
 (0)