Skip to content

Commit

Permalink
Merge pull request #122 from feltech/work/53-defaultEntityReference
Browse files Browse the repository at this point in the history
Support `defaultEntityReference`
  • Loading branch information
feltech authored Aug 29, 2024
2 parents 1a78d03 + 0b76500 commit 2f58d9b
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 1 deletion.
38 changes: 38 additions & 0 deletions plugin/openassetio_manager_bal/BasicAssetLibraryInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ def hasCapability(self, capability):
ManagerInterface.Capability.kPublishing,
ManagerInterface.Capability.kRelationshipQueries,
ManagerInterface.Capability.kExistenceQueries,
ManagerInterface.Capability.kDefaultEntityReferences,
):
return True

Expand Down Expand Up @@ -233,6 +234,41 @@ def managementPolicy(self, traitSets, access, context, hostSession):
def isEntityReferenceString(self, someString, hostSession):
return someString.startswith(self.__entity_refrence_prefix())

@simulated_delay
def defaultEntityReference(
self, traitSets, defaultEntityAccess, context, hostSession, successCallback, errorCallback
):
if not self.hasCapability(self.Capability.kDefaultEntityReferences):
super().defaultEntityReference(
traitSets,
defaultEntityAccess,
context,
hostSession,
successCallback,
errorCallback,
)
return

for idx, trait_set in enumerate(traitSets):
try:
entity_name = bal.default_entity(
trait_set, kAccessNames[defaultEntityAccess], self.__library
)
entity_ref = None
# Entity can legitimately be None, meaning query was OK
# but there is no suitable default.
if entity_name is not None:
entity_ref = self.__build_entity_ref(
bal.EntityInfo(
name=entity_name,
access=kAccessNames[defaultEntityAccess],
version=None,
)
)
successCallback(idx, entity_ref)
except Exception as exc: # pylint: disable=broad-except
self.__handle_exception(exc, idx, errorCallback)

@simulated_delay
def entityExists(self, entityRefs, context, _hostSession, successCallback, errorCallback):
if not self.hasCapability(self.Capability.kExistenceQueries):
Expand Down Expand Up @@ -752,6 +788,8 @@ def __handle_exception(exc, idx, error_callback):
code = BatchElementError.ErrorCode.kEntityResolutionError
elif isinstance(exc, bal.InaccessibleEntity):
code = BatchElementError.ErrorCode.kEntityAccessError
elif isinstance(exc, bal.UnknownTraitSet):
code = BatchElementError.ErrorCode.kInvalidTraitSet
else:
raise exc

Expand Down
22 changes: 22 additions & 0 deletions plugin/openassetio_manager_bal/bal.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,19 @@ def exists(entity_info: EntityInfo, library: dict) -> bool:
return True


def default_entity(trait_set: Set[str], access: str, library: dict) -> str:
"""
Retrieves the default entity for the supplied trait set and access
mode, if one exists in the library, otherwise raises an exception.
"""
default_entities_for_access = library.get("defaultEntities", {}).get(access, [])
# Find the first default entity that matches the trait set
for default_entity_for_trait_set in default_entities_for_access:
if set(default_entity_for_trait_set["traits"]) == trait_set:
return default_entity_for_trait_set["entity"]
raise UnknownTraitSet(trait_set)


def entity(entity_info: EntityInfo, library: dict) -> Entity:
"""
Retrieves the Entity data addressed by the supplied EntityInfo
Expand Down Expand Up @@ -412,3 +425,12 @@ class InaccessibleEntity(RuntimeError):

def __init__(self, entity_info: EntityInfo):
super().__init__(f"Entity '{entity_info.name}' is inaccessible for {entity_info.access}")


class UnknownTraitSet(RuntimeError):
"""
An exception raised when BAL doesn't understand a given trait set.
"""

def __init__(self, trait_set: Set[str]):
super().__init__(f"Unknown trait set {trait_set}")
69 changes: 69 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,75 @@
"required": ["read", "write"],
"additionalProperties": false
},
"defaultEntities": {
"description": "A mapping of intended access and trait set to appropriate default entity",
"type": "object",
"properties": {
"read": {
"type": "array",
"items": {
"type": "object",
"properties": {
"traits": {
"type": "array",
"items": {
"type": "string"
}
},
"entity": {
"type": ["string", "null"]
}
},
"required": [
"traits",
"entity"
]
}
},
"write": {
"type": "array",
"items": {
"type": "object",
"properties": {
"traits": {
"type": "array",
"items": {
"type": "string"
}
},
"entity": {
"type": ["string", "null"]
}
},
"required": [
"traits",
"entity"
]
}
},
"createRelated": {
"type": "array",
"items": {
"type": "object",
"properties": {
"traits": {
"type": "array",
"items": {
"type": "string"
}
},
"entity": {
"type": ["string", "null"]
}
},
"required": [
"traits",
"entity"
]
}
}
}
},
"entities": {
"description": "The entities in the library, they key is used as the entity name.",
"type": "object",
Expand Down
82 changes: 81 additions & 1 deletion tests/bal_business_logic_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,12 +380,12 @@ class Test_hasCapability_default(FixtureAugmentedTestCase):
def test_when_hasCapability_called_then_expected_capabilities_reported(self):
self.assertFalse(self._manager.hasCapability(Manager.Capability.kStatefulContexts))
self.assertFalse(self._manager.hasCapability(Manager.Capability.kCustomTerminology))
self.assertFalse(self._manager.hasCapability(Manager.Capability.kDefaultEntityReferences))

self.assertTrue(self._manager.hasCapability(Manager.Capability.kResolution))
self.assertTrue(self._manager.hasCapability(Manager.Capability.kPublishing))
self.assertTrue(self._manager.hasCapability(Manager.Capability.kRelationshipQueries))
self.assertTrue(self._manager.hasCapability(Manager.Capability.kExistenceQueries))
self.assertTrue(self._manager.hasCapability(Manager.Capability.kDefaultEntityReferences))

def test_when_hasCapability_called_on_managerInterface_then_has_mandatory_capabilities(self):
interface = BasicAssetLibraryInterface()
Expand Down Expand Up @@ -615,6 +615,86 @@ def test_when_read_only_entity_then_EntityAccessError_returned(self):
self.assertEqual(actual_result, expected_result)


class Test_defaultEntityReference(FixtureAugmentedTestCase):
"""
Tests for the defaultEntityReference method.
Uses the `defaultEntities` entry in library_apiComplianceSuite.json.
"""

def test_when_read_trait_set_known_then_expected_reference_returned(self):
expected = [
self._manager.createEntityReference("bal:///a_default_read_entity_for_a_and_b"),
self._manager.createEntityReference("bal:///a_default_read_entity_for_b_and_c"),
]
access = DefaultEntityAccess.kRead

self.assert_expected_entity_refs_for_access(expected, access)

def test_when_write_trait_set_known_then_expected_reference_returned(self):
expected = [
self._manager.createEntityReference("bal:///a_default_write_entity_for_a_and_b"),
self._manager.createEntityReference("bal:///a_default_write_entity_for_b_and_c"),
]
access = DefaultEntityAccess.kWrite

self.assert_expected_entity_refs_for_access(expected, access)

def test_when_createRelated_trait_set_known_then_expected_reference_returned(self):
expected = [
self._manager.createEntityReference("bal:///a_default_relatable_entity_for_a_and_b"),
self._manager.createEntityReference("bal:///a_default_relatable_entity_for_b_and_c"),
]
access = DefaultEntityAccess.kCreateRelated

self.assert_expected_entity_refs_for_access(expected, access)

def test_when_no_default_then_entity_ref_is_None(self):
results = [0] # Don't initialise to None because that's the value we expect.

self._manager.defaultEntityReference(
[{"c", "d"}],
DefaultEntityAccess.kRead,
self.createTestContext(),
lambda idx, value: operator.setitem(results, idx, value),
lambda idx, error: self.fail("defaultEntityReference should not fail"),
)

[actual] = results

self.assertIsNone(actual)

def test_when_trait_set_not_known_then_InvalidTraitSet_error(self):
results = [None]

self._manager.defaultEntityReference(
[{"a", "b", "c"}],
DefaultEntityAccess.kRead,
self.createTestContext(),
lambda idx, value: self.fail("defaultEntityReference should not succeed"),
lambda idx, error: operator.setitem(results, idx, error),
)

[actual] = results

self.assertIsInstance(actual, BatchElementError)
self.assertEqual(actual.code, BatchElementError.ErrorCode.kInvalidTraitSet)
self.assertRegex(actual.message, r"^Unknown trait set {'[abc]', '[abc]', '[abc]'}")

def assert_expected_entity_refs_for_access(self, expected, access):
actual = [None, None]

self._manager.defaultEntityReference(
[{"a", "b"}, {"b", "c"}],
access,
self.createTestContext(),
lambda idx, value: operator.setitem(actual, idx, value),
lambda idx, error: self.fail("defaultEntityReference should not fail"),
)

self.assertEqual(actual, expected)


class Test_resolve(FixtureAugmentedTestCase):
"""
Tests that resolution returns the expected values.
Expand Down
57 changes: 57 additions & 0 deletions tests/resources/library_apiComplianceSuite.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,63 @@
"default": {}
}
},
"defaultEntities": {
"read": [
{
"traits": [
"b",
"c"
],
"entity": "a_default_read_entity_for_b_and_c"
},
{
"traits": [
"a",
"b"
],
"entity": "a_default_read_entity_for_a_and_b"
},
{
"traits": [
"c",
"d"
],
"entity": null
}
],
"write": [
{
"traits": [
"b",
"c"
],
"entity": "a_default_write_entity_for_b_and_c"
},
{
"traits": [
"a",
"b"
],
"entity": "a_default_write_entity_for_a_and_b"
}
],
"createRelated": [
{
"traits": [
"b",
"c"
],
"entity": "a_default_relatable_entity_for_b_and_c"
},
{
"traits": [
"a",
"b"
],
"entity": "a_default_relatable_entity_for_a_and_b"
}
]
},
"entities": {
"anAsset⭐︎": {
"versions": [
Expand Down

0 comments on commit 2f58d9b

Please sign in to comment.