Skip to content

Commit 575eb08

Browse files
committed
SG-43999: add custom entity config features
1 parent db056df commit 575eb08

3 files changed

Lines changed: 295 additions & 0 deletions

File tree

docs/reference.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ The documentation for all of the methods you'll need in your scripts lives in he
105105
Shotgun.schema
106106
Shotgun.entity_types
107107

108+
.. rubric:: Custom Entity Configuration
109+
110+
.. autosummary::
111+
:nosignatures:
112+
113+
Shotgun.custom_entity_read
114+
Shotgun.custom_entity_enable
115+
Shotgun.custom_entity_update
116+
Shotgun.custom_entity_disable
117+
108118

109119
Connection & Authentication
110120
===========================
@@ -192,6 +202,17 @@ Methods allow you to introspect and modify the Shotgun schema.
192202
.. automethod:: Shotgun.schema
193203
.. automethod:: Shotgun.entity_types
194204

205+
Custom Entity Configuration
206+
===========================
207+
208+
Methods to read and configure Custom Entities at the site level. They require administrator
209+
privileges and a server running v8.88.0 or higher.
210+
211+
.. automethod:: Shotgun.custom_entity_read
212+
.. automethod:: Shotgun.custom_entity_enable
213+
.. automethod:: Shotgun.custom_entity_update
214+
.. automethod:: Shotgun.custom_entity_disable
215+
195216
**********
196217
Exceptions
197218
**********

shotgun_api3/shotgun.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,14 @@ def ensure_return_image_urls_support(self) -> bool:
329329
{"version": (3, 3, 0), "label": "return thumbnail URLs"}, False
330330
)
331331

332+
def ensure_custom_entity_config_support(self) -> None:
333+
"""
334+
Ensures server has support for the custom entity config API (read, enable, update, disable), added in v8.88.0.
335+
"""
336+
self._ensure_support(
337+
{"version": (8, 88, 0), "label": "custom entity config API"}
338+
)
339+
332340
def __str__(self) -> str:
333341
return "ServerCapabilities: host %s, version %s, is_dev %s" % (
334342
self.host,
@@ -3671,6 +3679,155 @@ def user_subscriptions_create(
36713679

36723680
return response.get("status") == "success"
36733681

3682+
def custom_entity_read(self, entity_type: str) -> Dict[str, Any]:
3683+
"""
3684+
Read the current configuration of a Custom Entity.
3685+
3686+
>>> sg.custom_entity_read("CustomEntity08")
3687+
{
3688+
"entity_type": "CustomEntity08",
3689+
"enabled": True,
3690+
"display_name": "My Shots",
3691+
"entity_config": {"enable_tasks": True, ...}
3692+
}
3693+
3694+
:param str entity_type: The Custom Entity type to read, in its singular
3695+
CamelCase form (e.g. ``"CustomEntity08"``). Required.
3696+
:returns: The entity config snapshot dict with ``entity_type``, ``enabled``,
3697+
``display_name``, and ``entity_config``.
3698+
:rtype: dict
3699+
:raises shotgun_api3.ShotgunError: if the entity type is invalid (fault code 104).
3700+
"""
3701+
self.server_caps.ensure_custom_entity_config_support()
3702+
3703+
return self._call_rpc("custom_entity_read", {"entity_type": entity_type})
3704+
3705+
def custom_entity_enable(
3706+
self,
3707+
entity_type: str,
3708+
display_name: Optional[str] = None,
3709+
entity_config: Optional[Dict[str, bool]] = None,
3710+
) -> Dict[str, Any]:
3711+
"""
3712+
Enable a Custom Entity.
3713+
3714+
>>> sg.custom_entity_enable(
3715+
... "CustomEntity08",
3716+
... display_name="My Shots",
3717+
... entity_config={"enable_tasks": True},
3718+
... )
3719+
{
3720+
"entity_type": "CustomEntity08",
3721+
"enabled": True,
3722+
"display_name": "My Shots",
3723+
"entity_config": {"enable_tasks": True, ...}
3724+
}
3725+
3726+
:param str entity_type: The Custom Entity type to enable, in its singular
3727+
CamelCase form (e.g. ``"CustomEntity08"``). Required.
3728+
:param str display_name: Optional display name for the entity.
3729+
:param dict entity_config: Optional dict of feature flag booleans. Only the
3730+
flags present are mutated; omitted flags are left unchanged.
3731+
Keys and boolean values are passed through as-is. Recognized flags:
3732+
- ``enable_tasks`` (default: ``False``)
3733+
- ``enable_versions`` (default: ``False``)
3734+
- ``enable_publishes`` (default: ``False``)
3735+
- ``enable_detail_page`` (default: ``True``)
3736+
- ``include_in_search`` (default: ``False``)
3737+
- ``include_in_global_menu`` (default: ``True``)
3738+
:returns: The entity config snapshot dict with ``entity_type``, ``enabled``,
3739+
``display_name``, and ``entity_config``.
3740+
:rtype: dict
3741+
:raises shotgun_api3.ShotgunError: if the entity type is invalid or already
3742+
enabled (fault code 104).
3743+
"""
3744+
self.server_caps.ensure_custom_entity_config_support()
3745+
3746+
params = {"entity_type": entity_type}
3747+
if display_name is not None:
3748+
params["display_name"] = display_name
3749+
if entity_config is not None:
3750+
params["entity_config"] = entity_config
3751+
3752+
return self._call_rpc("custom_entity_enable", params)
3753+
3754+
def custom_entity_update(
3755+
self,
3756+
entity_type: str,
3757+
display_name: Optional[str] = None,
3758+
entity_config: Optional[Dict[str, bool]] = None,
3759+
) -> Dict[str, Any]:
3760+
"""
3761+
Update an already-enabled Custom Entity's display name and/or feature flags.
3762+
3763+
>>> sg.custom_entity_update("CustomEntity08", display_name="Episode")
3764+
{
3765+
"entity_type": "CustomEntity08",
3766+
"enabled": True,
3767+
"display_name": "Renamed",
3768+
"entity_config": {...}
3769+
}
3770+
3771+
:param str entity_type: The Custom Entity type to update, in its singular
3772+
CamelCase form. The entity must already be enabled. Required.
3773+
:param str display_name: Optional new display name for the entity.
3774+
:param dict entity_config: Optional dict of feature flag booleans. Only the
3775+
flags present are mutated; omitted flags are left unchanged. Keys and
3776+
boolean values are passed through as-is.
3777+
:returns: The updated entity config snapshot dict.
3778+
:rtype: dict
3779+
:raises shotgun_api3.ShotgunError: if the entity type is invalid or not
3780+
enabled (fault code 104).
3781+
"""
3782+
self.server_caps.ensure_custom_entity_config_support()
3783+
3784+
params = {"entity_type": entity_type}
3785+
if display_name is not None:
3786+
params["display_name"] = display_name
3787+
if entity_config is not None:
3788+
params["entity_config"] = entity_config
3789+
3790+
return self._call_rpc("custom_entity_update", params)
3791+
3792+
def custom_entity_disable(
3793+
self, entity_type: str, force: bool = False
3794+
) -> Dict[str, Any]:
3795+
"""
3796+
Disable an enabled Custom Entity, clearing its feature flags.
3797+
3798+
Disabling a Custom Entity that has existing records does **not** delete the
3799+
data, but it does make the data inaccessible: the records will not appear in
3800+
the UI, will not be returned via the API, and any fields on other entities
3801+
that link to it become broken references. Because this is destructive in
3802+
effect, the server refuses to disable an entity that still has records unless
3803+
``force`` is set, and the error reports how many records were found.
3804+
3805+
>>> sg.custom_entity_disable("CustomEntity08")
3806+
{
3807+
"entity_type": "CustomEntity08",
3808+
"enabled": False,
3809+
"display_name": "My Shots"
3810+
}
3811+
3812+
:param str entity_type: The Custom Entity type to disable, in its singular
3813+
CamelCase form. The entity must already be enabled. Required.
3814+
:param bool force: Disable the entity even though it still has records.
3815+
Defaults to ``False``, which makes the call fail rather than render
3816+
existing data unreachable.
3817+
:returns: The entity config snapshot dict with ``enabled`` set to ``False``.
3818+
:rtype: dict
3819+
:raises shotgun_api3.ShotgunError: if the entity type is invalid or not
3820+
enabled (fault code 104), or if the entity still has records and
3821+
``force`` was not set (fault code 104).
3822+
"""
3823+
self.server_caps.ensure_custom_entity_config_support()
3824+
3825+
params = {"entity_type": entity_type}
3826+
if force:
3827+
params["force"] = True
3828+
3829+
return self._call_rpc("custom_entity_disable", params)
3830+
36743831
def _build_opener(self, handler) -> urllib.request.OpenerDirector:
36753832
"""
36763833
Build urllib2 opener with appropriate proxy handler.

tests/test_unit.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,5 +854,122 @@ def test_urlib(self):
854854
assert response is not None
855855

856856

857+
class CustomEntityConfigTestBase(unittest.TestCase):
858+
"""Shared setup for the custom entity config API test cases.
859+
860+
The custom_entity_* methods are gated on server version 8.88.0"""
861+
862+
def setUp(self):
863+
self.sg = api.Shotgun(
864+
"http://server_path", "script_name", "api_key", connect=False
865+
)
866+
self.set_server_version([8, 88, 0])
867+
868+
def set_server_version(self, version):
869+
self.sg._server_caps = api.shotgun.ServerCapabilities(
870+
self.sg.config.server, {"version": version}
871+
)
872+
873+
874+
class TestShotgunCustomEntityRead(CustomEntityConfigTestBase):
875+
"""Test case for Shotgun.custom_entity_read"""
876+
877+
@mock.patch("shotgun_api3.Shotgun._call_rpc")
878+
def test_entity_type_sent(self, call_rpc):
879+
self.sg.custom_entity_read("CustomEntity08")
880+
self.assertEqual("custom_entity_read", call_rpc.call_args[0][0])
881+
self.assertEqual({"entity_type": "CustomEntity08"}, call_rpc.call_args[0][1])
882+
883+
884+
class TestShotgunCustomEntityEnable(CustomEntityConfigTestBase):
885+
"""Test case for Shotgun.custom_entity_enable"""
886+
887+
@mock.patch("shotgun_api3.Shotgun._call_rpc")
888+
def test_optional_params_omitted_by_default(self, call_rpc):
889+
self.sg.custom_entity_enable("CustomEntity08")
890+
self.assertEqual("custom_entity_enable", call_rpc.call_args[0][0])
891+
self.assertEqual({"entity_type": "CustomEntity08"}, call_rpc.call_args[0][1])
892+
893+
@mock.patch("shotgun_api3.Shotgun._call_rpc")
894+
def test_optional_params_sent_when_set(self, call_rpc):
895+
entity_config = {"enable_tasks": True, "include_in_search": False}
896+
self.sg.custom_entity_enable(
897+
"CustomEntity08", display_name="My Shots", entity_config=entity_config
898+
)
899+
self.assertEqual(
900+
{
901+
"entity_type": "CustomEntity08",
902+
"display_name": "My Shots",
903+
"entity_config": entity_config,
904+
},
905+
call_rpc.call_args[0][1],
906+
)
907+
908+
@mock.patch("shotgun_api3.Shotgun._call_rpc")
909+
def test_empty_optional_params_sent(self, call_rpc):
910+
"""Empty values are distinct from omitted ones and must reach the server."""
911+
self.sg.custom_entity_enable(
912+
"CustomEntity08", display_name="", entity_config={}
913+
)
914+
self.assertEqual(
915+
{"entity_type": "CustomEntity08", "display_name": "", "entity_config": {}},
916+
call_rpc.call_args[0][1],
917+
)
918+
919+
920+
class TestShotgunCustomEntityUpdate(CustomEntityConfigTestBase):
921+
"""Test case for Shotgun.custom_entity_update"""
922+
923+
@mock.patch("shotgun_api3.Shotgun._call_rpc")
924+
def test_optional_params_omitted_by_default(self, call_rpc):
925+
self.sg.custom_entity_update("CustomEntity08")
926+
self.assertEqual("custom_entity_update", call_rpc.call_args[0][0])
927+
self.assertEqual({"entity_type": "CustomEntity08"}, call_rpc.call_args[0][1])
928+
929+
@mock.patch("shotgun_api3.Shotgun._call_rpc")
930+
def test_display_name_sent_without_entity_config(self, call_rpc):
931+
self.sg.custom_entity_update("CustomEntity08", display_name="Episode")
932+
self.assertEqual(
933+
{"entity_type": "CustomEntity08", "display_name": "Episode"},
934+
call_rpc.call_args[0][1],
935+
)
936+
937+
@mock.patch("shotgun_api3.Shotgun._call_rpc")
938+
def test_entity_config_sent_without_display_name(self, call_rpc):
939+
entity_config = {"enable_versions": False}
940+
self.sg.custom_entity_update("CustomEntity08", entity_config=entity_config)
941+
self.assertEqual(
942+
{"entity_type": "CustomEntity08", "entity_config": entity_config},
943+
call_rpc.call_args[0][1],
944+
)
945+
946+
@mock.patch("shotgun_api3.Shotgun._call_rpc")
947+
def test_empty_optional_params_sent(self, call_rpc):
948+
"""Empty values are distinct from omitted ones and must reach the server."""
949+
self.sg.custom_entity_update(
950+
"CustomEntity08", display_name="", entity_config={}
951+
)
952+
self.assertEqual(
953+
{"entity_type": "CustomEntity08", "display_name": "", "entity_config": {}},
954+
call_rpc.call_args[0][1],
955+
)
956+
957+
958+
class TestShotgunCustomEntityDisable(CustomEntityConfigTestBase):
959+
"""Test case for Shotgun.custom_entity_disable"""
960+
961+
@mock.patch("shotgun_api3.Shotgun._call_rpc")
962+
def test_force_omitted_by_default(self, call_rpc):
963+
self.sg.custom_entity_disable("CustomEntity08")
964+
self.assertEqual({"entity_type": "CustomEntity08"}, call_rpc.call_args[0][1])
965+
966+
@mock.patch("shotgun_api3.Shotgun._call_rpc")
967+
def test_force_sent_when_set(self, call_rpc):
968+
self.sg.custom_entity_disable("CustomEntity08", force=True)
969+
self.assertEqual(
970+
{"entity_type": "CustomEntity08", "force": True}, call_rpc.call_args[0][1]
971+
)
972+
973+
857974
if __name__ == "__main__":
858975
unittest.main()

0 commit comments

Comments
 (0)