From 6d56c3a6073fd583f2201eb309fbe24fdb4dfffe Mon Sep 17 00:00:00 2001 From: David Feltell Date: Wed, 10 Apr 2024 16:59:20 +0100 Subject: [PATCH 1/4] [Docs] Trait versioning design sketch Closes #88. As due diligence before going ahead with an implementation, we need to sketch out what a versioned trait package would look like, and how hosts and managers would interact with it. So add a contrived trait package with two versions. The "generated" view classes have been manually created to reflect what the `traitgen` output should be, once implemented. These view classes are then used in a tutorial notebook that describes how one would go about working with versioned traits. This work may be abandoned or adapted in favour of more concrete documentation as the feature is developed. Signed-off-by: David Feltell --- .../openassetio_example/__init__.py | 7 + .../openassetio_example/v1/__init__.py | 8 + .../v1/specifications/__init__.py | 7 + .../v1/specifications/example.py | 77 ++ .../openassetio_example/v1/traits/__init__.py | 7 + .../openassetio_example/v1/traits/example.py | 259 +++++ .../openassetio_example/v2/__init__.py | 8 + .../v2/specifications/__init__.py | 7 + .../v2/specifications/example.py | 77 ++ .../openassetio_example/v2/traits/__init__.py | 7 + .../openassetio_example/v2/traits/example.py | 257 +++++ .../working_with_trait_versions/v1.yml | 62 ++ .../working_with_trait_versions/v2.yml | 61 ++ examples/working_with_trait_versions.ipynb | 979 ++++++++++++++++++ 14 files changed, 1823 insertions(+) create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/__init__.py create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v1/__init__.py create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/__init__.py create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/example.py create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v1/traits/__init__.py create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v1/traits/example.py create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v2/__init__.py create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/__init__.py create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/example.py create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v2/traits/__init__.py create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v2/traits/example.py create mode 100644 examples/resources/working_with_trait_versions/v1.yml create mode 100644 examples/resources/working_with_trait_versions/v2.yml create mode 100644 examples/working_with_trait_versions.ipynb diff --git a/examples/resources/working_with_trait_versions/openassetio_example/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/__init__.py new file mode 100644 index 0000000..0b1f0d8 --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/__init__.py @@ -0,0 +1,7 @@ +""" +An example traits schema +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from .v2 import * diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v1/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/v1/__init__.py new file mode 100644 index 0000000..a755489 --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/v1/__init__.py @@ -0,0 +1,8 @@ +""" +An example traits schema +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from . import traits +from . import specifications diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/__init__.py new file mode 100644 index 0000000..06aaee0 --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/__init__.py @@ -0,0 +1,7 @@ +""" +Specifications defined in the 'openassetio-example' package. +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from . import example diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/example.py b/examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/example.py new file mode 100644 index 0000000..17c4409 --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/example.py @@ -0,0 +1,77 @@ + +""" +Specification definitions in the 'example' namespace. + +Test specifications. +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from openassetio.trait import TraitsData + + +from .. import traits + + + +class ExampleSpecification: + """ + An example. + Usage: entity + """ + kTraitSet = { + # 'openassetio-example:example.Unchanged' + traits.example.UnchangedTrait.kId, + # 'openassetio-example:example.Updated' + traits.example.UpdatedTrait.kId, + + } + + def __init__(self, traitsData): + """ + Constructs the specification as a view on the supplied + shared @fqref{TraitsData} "TraitsData" instance. + + @param traitsData @fqref{TraitsData} "TraitsData" + + @warning Specifications are always a view on the supplied data, + which is held by reference. Any changes made to the data will be + visible to any other specifications or @ref trait "traits" that + wrap the same TraitsData instance. + """ + if not isinstance(traitsData, TraitsData): + raise TypeError("Specifications must be constructed with a TraitsData instance") + self.__data = traitsData + + def traitsData(self): + """ + Returns the underlying (shared) @fqref{TraitsData} "TraitsData" + instance held by this specification. + """ + return self.__data + + @classmethod + def create(cls): + """ + Returns a new instance of the Specification, holding a new + @fqref{TraitsData} "TraitsData" instance, pre-populated with all + of the specifications traits. + """ + data = TraitsData(cls.kTraitSet) + return cls(data) + + + def unchangedTrait(self): + """ + Returns the view for the 'openassetio-example:example.Unchanged' trait wrapped around + the data held in this instance. + """ + return traits.example.UnchangedTrait(self.traitsData()) + + def updatedTrait(self): + """ + Returns the view for the 'openassetio-example:example.Updated' trait wrapped around + the data held in this instance. + """ + return traits.example.UpdatedTrait(self.traitsData()) + \ No newline at end of file diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v1/traits/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/v1/traits/__init__.py new file mode 100644 index 0000000..190df00 --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/v1/traits/__init__.py @@ -0,0 +1,7 @@ +""" +Traits defined in the 'openassetio-example' package. +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from . import example diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v1/traits/example.py b/examples/resources/working_with_trait_versions/openassetio_example/v1/traits/example.py new file mode 100644 index 0000000..d0ae54e --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/v1/traits/example.py @@ -0,0 +1,259 @@ + +""" +Trait definitions in the 'example' namespace. + +Example namespace +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from typing import Union + +from openassetio.trait import TraitsData + + +class RemovedTrait: + """ + An example. + Usage: entity, locale, relationship + """ + kId = "openassetio-example:example.Removed.v1" + + def __init__(self, traitsData): + """ + Construct this trait view, wrapping the given data. + + @param traitsData @fqref{TraitsData}} "TraitsData" The target + data that holds/will hold the traits properties. + """ + self.__data = traitsData + + def isImbued(self): + """ + Checks whether the data this trait has been applied to + actually has this trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return self.isImbuedTo(self.__data) + + @classmethod + def isImbuedTo(cls, traitsData): + """ + Checks whether the given data actually has this trait. + @param traitsData: Data to check for trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return traitsData.hasTrait(cls.kId) + + def imbue(self): + """ + Adds this trait to the held data. + + If the data already has this trait, it is a no-op. + """ + self.__data.addTrait(self.kId) + + @classmethod + def imbueTo(cls, traitsData): + """ + Adds this trait to the provided data. + + If the data already has this trait, it is a no-op. + """ + traitsData.addTrait(cls.kId) + + + + +class UnchangedTrait: + """ + An example. + Usage: entity, locale, relationship + """ + kId = "openassetio-example:example.Unchanged.v1" + + def __init__(self, traitsData): + """ + Construct this trait view, wrapping the given data. + + @param traitsData @fqref{TraitsData}} "TraitsData" The target + data that holds/will hold the traits properties. + """ + self.__data = traitsData + + def isImbued(self): + """ + Checks whether the data this trait has been applied to + actually has this trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return self.isImbuedTo(self.__data) + + @classmethod + def isImbuedTo(cls, traitsData): + """ + Checks whether the given data actually has this trait. + @param traitsData: Data to check for trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return traitsData.hasTrait(cls.kId) + + def imbue(self): + """ + Adds this trait to the held data. + + If the data already has this trait, it is a no-op. + """ + self.__data.addTrait(self.kId) + + @classmethod + def imbueTo(cls, traitsData): + """ + Adds this trait to the provided data. + + If the data already has this trait, it is a no-op. + """ + traitsData.addTrait(cls.kId) + + + + +class UpdatedTrait: + """ + An example. + Usage: entity, locale, relationship + """ + kId = "openassetio-example:example.Updated.v1" + + def __init__(self, traitsData): + """ + Construct this trait view, wrapping the given data. + + @param traitsData @fqref{TraitsData}} "TraitsData" The target + data that holds/will hold the traits properties. + """ + self.__data = traitsData + + def isImbued(self): + """ + Checks whether the data this trait has been applied to + actually has this trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return self.isImbuedTo(self.__data) + + @classmethod + def isImbuedTo(cls, traitsData): + """ + Checks whether the given data actually has this trait. + @param traitsData: Data to check for trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return traitsData.hasTrait(cls.kId) + + def imbue(self): + """ + Adds this trait to the held data. + + If the data already has this trait, it is a no-op. + """ + self.__data.addTrait(self.kId) + + @classmethod + def imbueTo(cls, traitsData): + """ + Adds this trait to the provided data. + + If the data already has this trait, it is a no-op. + """ + traitsData.addTrait(cls.kId) + + + def setPropertyToKeep(self, propertyToKeep: str): + """ + Sets the propertyToKeep property. + + A property that is unchanged between versions. + """ + if not isinstance(propertyToKeep, str): + raise TypeError("propertyToKeep must be a 'str'.") + self.__data.setTraitProperty(self.kId, "propertyToKeep", propertyToKeep) + + def getPropertyToKeep(self, defaultValue: str=None) -> Union[str, None]: + """ + Gets the value of the propertyToKeep property or the supplied default. + + A property that is unchanged between versions. + """ + value = self.__data.getTraitProperty(self.kId, "propertyToKeep") + if value is None: + return defaultValue + + if not isinstance(value, str): + if defaultValue is None: + raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'str'.") + return defaultValue + return value + + def setPropertyToRemove(self, propertyToRemove: bool): + """ + Sets the propertyToRemove property. + + A defunct property that should be removed in the next version. + """ + if not isinstance(propertyToRemove, bool): + raise TypeError("propertyToRemove must be a 'bool'.") + self.__data.setTraitProperty(self.kId, "propertyToRemove", propertyToRemove) + + def getPropertyToRemove(self, defaultValue: bool=None) -> Union[bool, None]: + """ + Gets the value of the propertyToRemove property or the supplied default. + + A defunct property that should be removed in the next version. + """ + value = self.__data.getTraitProperty(self.kId, "propertyToRemove") + if value is None: + return defaultValue + + if not isinstance(value, bool): + if defaultValue is None: + raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'bool'.") + return defaultValue + return value + + def setPropertyToRename(self, propertyToRename: bool): + """ + Sets the propertyToRename property. + + A property that has an inappropriate name and should be renamed + in the next version. + """ + if not isinstance(propertyToRename, bool): + raise TypeError("propertyToRename must be a 'bool'.") + self.__data.setTraitProperty(self.kId, "propertyToRename", propertyToRename) + + def getPropertyToRename(self, defaultValue: bool=None) -> Union[bool, None]: + """ + Gets the value of the propertyToRename property or the supplied default. + + A property that has an inappropriate name and should be renamed + in the next version. + """ + value = self.__data.getTraitProperty(self.kId, "propertyToRename") + if value is None: + return defaultValue + + if not isinstance(value, bool): + if defaultValue is None: + raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'bool'.") + return defaultValue + return value + + + diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v2/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/v2/__init__.py new file mode 100644 index 0000000..a755489 --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/v2/__init__.py @@ -0,0 +1,8 @@ +""" +An example traits schema +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from . import traits +from . import specifications diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/__init__.py new file mode 100644 index 0000000..06aaee0 --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/__init__.py @@ -0,0 +1,7 @@ +""" +Specifications defined in the 'openassetio-example' package. +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from . import example diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/example.py b/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/example.py new file mode 100644 index 0000000..4fee4bf --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/example.py @@ -0,0 +1,77 @@ + +""" +Specification definitions in the 'example' namespace. + +Test specifications. +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from openassetio.trait import TraitsData + + +from .. import traits + + + +class ExampleSpecification: + """ + An example specification. + Usage: entity + """ + kTraitSet = { + # 'openassetio-example:example.Unchanged' + traits.example.UnchangedTrait.kId, + # 'openassetio-example:example.Updated' + traits.example.UpdatedTrait.kId, + + } + + def __init__(self, traitsData): + """ + Constructs the specification as a view on the supplied + shared @fqref{TraitsData} "TraitsData" instance. + + @param traitsData @fqref{TraitsData} "TraitsData" + + @warning Specifications are always a view on the supplied data, + which is held by reference. Any changes made to the data will be + visible to any other specifications or @ref trait "traits" that + wrap the same TraitsData instance. + """ + if not isinstance(traitsData, TraitsData): + raise TypeError("Specifications must be constructed with a TraitsData instance") + self.__data = traitsData + + def traitsData(self): + """ + Returns the underlying (shared) @fqref{TraitsData} "TraitsData" + instance held by this specification. + """ + return self.__data + + @classmethod + def create(cls): + """ + Returns a new instance of the Specification, holding a new + @fqref{TraitsData} "TraitsData" instance, pre-populated with all + of the specifications traits. + """ + data = TraitsData(cls.kTraitSet) + return cls(data) + + + def unchangedTrait(self): + """ + Returns the view for the 'openassetio-example:example.Unchanged' trait wrapped around + the data held in this instance. + """ + return traits.example.UnchangedTrait(self.traitsData()) + + def updatedTrait(self): + """ + Returns the view for the 'openassetio-example:example.Updated' trait wrapped around + the data held in this instance. + """ + return traits.example.UpdatedTrait(self.traitsData()) + \ No newline at end of file diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/__init__.py new file mode 100644 index 0000000..190df00 --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/__init__.py @@ -0,0 +1,7 @@ +""" +Traits defined in the 'openassetio-example' package. +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from . import example diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/example.py b/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/example.py new file mode 100644 index 0000000..9463e35 --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/example.py @@ -0,0 +1,257 @@ + +""" +Trait definitions in the 'example' namespace. + +Example namespace +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from typing import Union + +from openassetio.trait import TraitsData + + +class AddedTrait: + """ + An example. + Usage: entity, locale, relationship + """ + kId = "openassetio-example:example.Added.v1" + + def __init__(self, traitsData): + """ + Construct this trait view, wrapping the given data. + + @param traitsData @fqref{TraitsData}} "TraitsData" The target + data that holds/will hold the traits properties. + """ + self.__data = traitsData + + def isImbued(self): + """ + Checks whether the data this trait has been applied to + actually has this trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return self.isImbuedTo(self.__data) + + @classmethod + def isImbuedTo(cls, traitsData): + """ + Checks whether the given data actually has this trait. + @param traitsData: Data to check for trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return traitsData.hasTrait(cls.kId) + + def imbue(self): + """ + Adds this trait to the held data. + + If the data already has this trait, it is a no-op. + """ + self.__data.addTrait(self.kId) + + @classmethod + def imbueTo(cls, traitsData): + """ + Adds this trait to the provided data. + + If the data already has this trait, it is a no-op. + """ + traitsData.addTrait(cls.kId) + + + + +class UnchangedTrait: + """ + An example. + Usage: entity, locale, relationship + """ + kId = "openassetio-example:example.Unchanged.v1" + + def __init__(self, traitsData): + """ + Construct this trait view, wrapping the given data. + + @param traitsData @fqref{TraitsData}} "TraitsData" The target + data that holds/will hold the traits properties. + """ + self.__data = traitsData + + def isImbued(self): + """ + Checks whether the data this trait has been applied to + actually has this trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return self.isImbuedTo(self.__data) + + @classmethod + def isImbuedTo(cls, traitsData): + """ + Checks whether the given data actually has this trait. + @param traitsData: Data to check for trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return traitsData.hasTrait(cls.kId) + + def imbue(self): + """ + Adds this trait to the held data. + + If the data already has this trait, it is a no-op. + """ + self.__data.addTrait(self.kId) + + @classmethod + def imbueTo(cls, traitsData): + """ + Adds this trait to the provided data. + + If the data already has this trait, it is a no-op. + """ + traitsData.addTrait(cls.kId) + + + + +class UpdatedTrait: + """ + An example. + Usage: entity, locale, relationship + """ + kId = "openassetio-example:example.Updated.v2" + + def __init__(self, traitsData): + """ + Construct this trait view, wrapping the given data. + + @param traitsData @fqref{TraitsData}} "TraitsData" The target + data that holds/will hold the traits properties. + """ + self.__data = traitsData + + def isImbued(self): + """ + Checks whether the data this trait has been applied to + actually has this trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return self.isImbuedTo(self.__data) + + @classmethod + def isImbuedTo(cls, traitsData): + """ + Checks whether the given data actually has this trait. + @param traitsData: Data to check for trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return traitsData.hasTrait(cls.kId) + + def imbue(self): + """ + Adds this trait to the held data. + + If the data already has this trait, it is a no-op. + """ + self.__data.addTrait(self.kId) + + @classmethod + def imbueTo(cls, traitsData): + """ + Adds this trait to the provided data. + + If the data already has this trait, it is a no-op. + """ + traitsData.addTrait(cls.kId) + + + def setPropertyThatWasAdded(self, propertyThatWasAdded: float): + """ + Sets the propertyThatWasAdded property. + + A new property added in the latest version. + """ + if not isinstance(propertyThatWasAdded, float): + raise TypeError("propertyThatWasAdded must be a 'float'.") + self.__data.setTraitProperty(self.kId, "propertyThatWasAdded", propertyThatWasAdded) + + def getPropertyThatWasAdded(self, defaultValue: float=None) -> Union[float, None]: + """ + Gets the value of the propertyThatWasAdded property or the supplied default. + + A new property added in the latest version. + """ + value = self.__data.getTraitProperty(self.kId, "propertyThatWasAdded") + if value is None: + return defaultValue + + if not isinstance(value, float): + if defaultValue is None: + raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'float'.") + return defaultValue + return value + + def setPropertyThatWasRenamed(self, propertyThatWasRenamed: bool): + """ + Sets the propertyThatWasRenamed property. + + A property that has been renamed. + """ + if not isinstance(propertyThatWasRenamed, bool): + raise TypeError("propertyThatWasRenamed must be a 'bool'.") + self.__data.setTraitProperty(self.kId, "propertyThatWasRenamed", propertyThatWasRenamed) + + def getPropertyThatWasRenamed(self, defaultValue: bool=None) -> Union[bool, None]: + """ + Gets the value of the propertyThatWasRenamed property or the supplied default. + + A property that has been renamed. + """ + value = self.__data.getTraitProperty(self.kId, "propertyThatWasRenamed") + if value is None: + return defaultValue + + if not isinstance(value, bool): + if defaultValue is None: + raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'bool'.") + return defaultValue + return value + + def setPropertyToKeep(self, propertyToKeep: str): + """ + Sets the propertyToKeep property. + + A property that is unchanged between versions. + """ + if not isinstance(propertyToKeep, str): + raise TypeError("propertyToKeep must be a 'str'.") + self.__data.setTraitProperty(self.kId, "propertyToKeep", propertyToKeep) + + def getPropertyToKeep(self, defaultValue: str=None) -> Union[str, None]: + """ + Gets the value of the propertyToKeep property or the supplied default. + + A property that is unchanged between versions. + """ + value = self.__data.getTraitProperty(self.kId, "propertyToKeep") + if value is None: + return defaultValue + + if not isinstance(value, str): + if defaultValue is None: + raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'str'.") + return defaultValue + return value + + + diff --git a/examples/resources/working_with_trait_versions/v1.yml b/examples/resources/working_with_trait_versions/v1.yml new file mode 100644 index 0000000..51b38cd --- /dev/null +++ b/examples/resources/working_with_trait_versions/v1.yml @@ -0,0 +1,62 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/OpenAssetIO/OpenAssetIO-TraitGen/main/python/openassetio_traitgen/schema.json +# yamllint disable-line rule:document-start +package: openassetio-example +version: 1 +description: An example traits schema + +traits: + example: + description: Example namespace + members: + Unchanged: + version: 1 + description: An example. + usage: + - entity + - locale + - relationship + + Removed: + version: 1 + description: An example. + usage: + - entity + - locale + - relationship + + Updated: + version: 1 + description: An example. + usage: + - entity + - locale + - relationship + properties: + propertyToKeep: + type: string + description: A property that is unchanged between versions. + propertyToRename: + type: boolean + description: > + A property that has an inappropriate name and should be renamed in the + next version. + propertyToRemove: + type: boolean + description: A defunct property that should be removed in the next version. + +specifications: + example: + description: Test specifications. + members: + + Example: + description: An example. + usage: + - entity + traitSet: + - namespace: example + name: Unchanged + version: 1 + - namespace: example + name: Updated + version: 1 diff --git a/examples/resources/working_with_trait_versions/v2.yml b/examples/resources/working_with_trait_versions/v2.yml new file mode 100644 index 0000000..c1f7b1b --- /dev/null +++ b/examples/resources/working_with_trait_versions/v2.yml @@ -0,0 +1,61 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/OpenAssetIO/OpenAssetIO-TraitGen/main/python/openassetio_traitgen/schema.json +# yamllint disable-line rule:document-start +package: openassetio-example +version: 2 +description: An example traits schema + +traits: + example: + description: Example namespace + members: + Unchanged: + version: 1 + description: An example. + usage: + - entity + - locale + - relationship + + Added: + version: 1 + description: An example. + usage: + - entity + - locale + - relationship + + Updated: + version: 2 + description: An example. + usage: + - entity + - locale + - relationship + properties: + propertyToKeep: + type: string + description: A property that is unchanged between versions. + propertyThatWasRenamed: + type: boolean + description: A property that has been renamed. + propertyThatWasAdded: + type: float + description: A new property added in the latest version. + +specifications: + example: + description: Test specifications. + members: + + Example: + description: An example specification. + usage: + - entity + traitSet: + - namespace: example + name: Unchanged + version: 1 + - namespace: example + name: Updated + version: 2 + diff --git a/examples/working_with_trait_versions.ipynb b/examples/working_with_trait_versions.ipynb new file mode 100644 index 0000000..80fefc9 --- /dev/null +++ b/examples/working_with_trait_versions.ipynb @@ -0,0 +1,979 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Hosts/Managers: Working with trait versions\n", + "\n", + "This notebook illustrates how a manager and host can work together despite using different trait versions.\n" + ], + "metadata": { + "collapsed": false + }, + "id": "07297a3d-048b-496b-adb0-8fdd67316021" + }, + { + "cell_type": "markdown", + "source": [ + "## Versioned traits" + ], + "metadata": { + "collapsed": false + }, + "id": "dbc81a5ffa923a5" + }, + { + "cell_type": "markdown", + "source": [ + "### Schema subpackages\n", + "\n", + "Trait packages generated by `traitgen` can include subpackages for all available schema versions, with the top-level namespace containing aliases to the most-recent available version.\n", + "\n", + "To illustrate this, we use the versioned trait mockups under `resources/working_with_trait_versions`. \n" + ], + "metadata": { + "collapsed": false + }, + "id": "c8f9847f13e25fa1" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from resources.working_with_trait_versions.openassetio_example import traits, specifications\n", + "from resources.working_with_trait_versions.openassetio_example import v1\n", + "from resources.working_with_trait_versions.openassetio_example import v2\n", + "\n", + "\n", + "assert traits is v2.traits\n", + "assert specifications is v2.specifications\n", + "\n", + "assert v1.traits is not v2.traits\n", + "assert v1.traits is not v2.specifications" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-17T13:35:09.501325Z", + "start_time": "2024-04-17T13:35:09.489263Z" + } + }, + "id": "ed3207872eaa89b0", + "execution_count": 1 + }, + { + "cell_type": "markdown", + "source": [ + "Host applications that bundle the trait package may use the non-versioned top-level package by default. \n", + "\n", + "For Python manager plugins, if \n", + "\n", + "* the host application's largest (most-recent) schema version does not match the schema version the Python plugin was developed against; and\n", + "* the Python plugin uses the default (top-level) traits/specifications packages\n", + "\n", + "the application may hit unexpected runtime errors. This is because the structure of trait/specification view classes may be different, or simply not exist.\n", + "\n", + "A Python manager plugin should therefore use a versioned namespace. This solves the problem when the plugin is loaded into an application that defaults to a _higher_ schema version than the plugin was developed for. \n", + "\n", + "However, if the host application's largest available schema version is too _low_ for the manager plugin, then using a versioned namespace would suffer an `ImportError` when attempting to `import` the (too new) versioned namespace. This can either be left to fail (better to fail early), or be tolerated within the plugin by catching the exception and falling back to an older version.\n", + "\n", + "C++ plugins have the trait view classes (privately) compiled into them, so do not depend on the schema version that the host application was built against. Incompatibilities only become apparent at runtime, when incoming trait data is found to be of a version unsupported by the manager or host." + ], + "metadata": { + "collapsed": false + }, + "id": "e96f0b057e4fd49c" + }, + { + "cell_type": "markdown", + "source": [ + "### Trait views within subpackages\n", + "\n", + "We imagine an industry where there are only 4 traits, `AddedTrait`, `RemovedTrait`, `UnchangedTrait` and `UpdatedTrait`, which are used across entities, relationships, policies and locales. The traits themselves have no meaning, they are named purely to give a hint as to how they change in subsequent trait schema versions.\n", + "\n", + "Within a subpackage, all traits within that subpackage's schema version are included, with unchanged traits being duplicated. Duplication may be replaced with aliasing in a future update of `traitgen`." + ], + "metadata": { + "collapsed": false + }, + "id": "26381c27fe243e62" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "assert v2.traits.example.UnchangedTrait is not v1.traits.example.UnchangedTrait\n", + "assert v2.traits.example.UpdatedTrait is not v1.traits.example.UpdatedTrait\n", + "\n", + "assert \"RemovedTrait\" not in v2.traits.example.__dict__\n", + "assert \"RemovedTrait\" in v1.traits.example.__dict__\n", + "\n", + "assert \"AddedTrait\" in v2.traits.example.__dict__\n", + "assert \"AddedTrait\" not in v1.traits.example.__dict__" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-17T13:54:03.525882Z", + "start_time": "2024-04-17T13:54:03.513393Z" + } + }, + "id": "1470d16817a82421", + "execution_count": 10 + }, + { + "cell_type": "markdown", + "source": [ + "Each trait encodes its version in its unique ID. \n", + "\n", + "The schema version is bumped whenever one or more traits or specifications are added/removed/updated, forming the next schema version. This means the maximum possible version of a trait is bounded by the top-level schema version (inclusive). Adding a new trait will result in a schema version bump, but the new trait itself will start at version 1." + ], + "metadata": { + "collapsed": false + }, + "id": "75a1877c0c9c3786" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "# An updated trait has a different ID in each subpackage.\n", + "assert v1.traits.example.UpdatedTrait.kId == \"openassetio-example:example.Updated.v1\"\n", + "assert v2.traits.example.UpdatedTrait.kId == \"openassetio-example:example.Updated.v2\"\n", + "\n", + "# A newly added trait starts at version 1, despite the schema\n", + "# (subpackage) version being greater than 1.\n", + "assert v2.traits.example.AddedTrait.kId == \"openassetio-example:example.Added.v1\"" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-17T13:35:09.531776Z", + "start_time": "2024-04-17T13:35:09.516191Z" + } + }, + "id": "a31f8a7263370714", + "execution_count": 3 + }, + { + "cell_type": "markdown", + "source": [ + "This means trait view classes from one schema version cannot be used to read traits of another version, unless the trait is unchanged between schema versions." + ], + "metadata": { + "collapsed": false + }, + "id": "6e6d3357806e5f67" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from openassetio.trait import TraitsData\n", + "\n", + "\n", + "v1_data, v2_data, unchanged_data = TraitsData(), TraitsData(), TraitsData()\n", + "\n", + "v1.traits.example.UpdatedTrait.imbueTo(v1_data)\n", + "v2.traits.example.UpdatedTrait.imbueTo(v2_data)\n", + "v1.traits.example.UnchangedTrait.imbueTo(unchanged_data)\n", + "\n", + "assert v1.traits.example.UpdatedTrait.isImbuedTo(v1_data) is True\n", + "assert v1.traits.example.UpdatedTrait.isImbuedTo(v2_data) is False\n", + "\n", + "assert v2.traits.example.UpdatedTrait.isImbuedTo(v1_data) is False\n", + "assert v2.traits.example.UpdatedTrait.isImbuedTo(v2_data) is True\n", + "\n", + "assert v1.traits.example.UnchangedTrait.isImbuedTo(unchanged_data) is True\n", + "assert v2.traits.example.UnchangedTrait.isImbuedTo(unchanged_data) is True" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-17T13:35:09.544703Z", + "start_time": "2024-04-17T13:35:09.533132Z" + } + }, + "id": "a147006dd4b9b453", + "execution_count": 4 + }, + { + "cell_type": "markdown", + "source": [ + "### Specifications\n", + "\n", + "Specifications are a way to document well-known sets of traits that categorize entities, relationships, locales, or policies. Agreement on these as an industry is crucial for effective interop.\n", + "\n", + "However, they do not have an independent version - their version is implicit in the (versioned) traits that they compose, and in the overall schema version where they are defined. A major consequence of this is that no specification version is embedded in the data itself.\n", + "\n", + "If the raison d'être of specifications is a way to have an industry-standard collection of trait sets, then is the exact version of those traits really important? On the assumption that a new version of a trait doesn't fundamentally change its meaning (otherwise it would be an entirely new trait), then it's reasonable to say that specifications are trait version agnostic.\n", + "\n", + "So specifications are invaluable as documentation of common trait sets. However, the auto-generateed `Specification` view classes should be used with caution: using a specification view class to categorize an entity may lead to unexpected false negatives when the trait versions do not line up.\n" + ], + "metadata": { + "collapsed": false + }, + "id": "ab4dde470a3808d4" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "entity_data = TraitsData(\n", + " {v1.traits.example.UnchangedTrait.kId, v1.traits.example.UpdatedTrait.kId})\n", + "\n", + "# As long as the trait set of the specification lines up with the\n", + "# incoming data, we can use it to categorize an entity.\n", + "is_an_example_entity = v1.specifications.example.ExampleSpecification.kTraitSet.issubset(\n", + " entity_data.traitSet())\n", + "assert is_an_example_entity is True\n", + "\n", + "# Conceptually, the entity is still an Example, but subsequent updates\n", + "# to the specification mean the version suffix on some trait IDs have\n", + "# updated and no longer match the incoming data, so we get a false\n", + "# negative.\n", + "is_an_example_entity = v2.specifications.example.ExampleSpecification.kTraitSet.issubset(\n", + " entity_data.traitSet())\n", + "assert is_an_example_entity is False\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-17T13:35:09.557999Z", + "start_time": "2024-04-17T13:35:09.545693Z" + } + }, + "id": "a94f4bcd14862e3a", + "execution_count": 5 + }, + { + "cell_type": "markdown", + "source": [ + "## Example \n", + "\n", + "The following sections will define a manager and a host and explore how they can communicate despite no prior knowledge of what trait versions each side will send.\n", + "\n", + "### Prerequisites\n", + "\n", + "First we must define some boilerplate." + ], + "metadata": { + "collapsed": false + }, + "id": "a06b8108bf6b24f1" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from openassetio.hostApi import HostInterface\n", + "from openassetio.managerApi import Host, HostSession\n", + "from openassetio.log import ConsoleLogger, SeverityFilter\n", + "\n", + "\n", + "class NotebookHostInterface(HostInterface):\n", + " def identifier(self):\n", + " return \"org.jupyter.notebook\"\n", + "\n", + " def displayName(self):\n", + " return \"Jupyter Notebook\"\n", + "\n", + "\n", + "host_session = HostSession(Host(NotebookHostInterface()), SeverityFilter(ConsoleLogger()))" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-17T13:35:09.575258Z", + "start_time": "2024-04-17T13:35:09.558968Z" + } + }, + "id": "592a08fb134655a", + "execution_count": 6 + }, + { + "cell_type": "markdown", + "source": [ + "### Manager \n", + "\n", + "In the following a manager implementation is defined that relies solely on the versioned trait mockups under `resources/working_with_trait_versions`. \n", + "\n", + "The idea is simply to tease out possible patterns of versioned trait combinations and access patterns that might cause problems for implementors. As such, the semantics are nonsense, but hopefully the branching logic is roughly representative." + ], + "metadata": { + "collapsed": false + }, + "id": "6a4b40c3f5f306e" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from openassetio.managerApi import ManagerInterface, EntityReferencePagerInterface\n", + "from openassetio import errors, access, EntityReference\n", + "\n", + "\n", + "an_entity_ref_str = \"example://entity\"\n", + "\n", + "\n", + "class ExampleManagerInterface(ManagerInterface):\n", + "\n", + " def identifier(self):\n", + " return \"org.openassetio.example.manager\"\n", + "\n", + " def displayName(self):\n", + " return \"Example Manager\"\n", + "\n", + " def hasCapability(self, capability):\n", + " return capability in (\n", + " ManagerInterface.Capability.kEntityReferenceIdentification,\n", + " ManagerInterface.Capability.kManagementPolicyQueries,\n", + " ManagerInterface.Capability.kEntityTraitIntrospection,\n", + " ManagerInterface.Capability.kResolution,\n", + " ManagerInterface.Capability.kPublishing,\n", + " ManagerInterface.Capability.kRelationshipQueries,\n", + " ManagerInterface.Capability.kDefaultEntityReferences,\n", + " )\n", + "\n", + " def isEntityReferenceString(self, someString, _hostSession):\n", + " return someString.startswith(\"example://\")\n", + "\n", + " def managementPolicy(self, traitSets, policyAccess, context, _hostSession):\n", + " # Initialise default empty response, to be filled in below.\n", + " policy_datas = [TraitsData() for _ in traitSets]\n", + "\n", + " # We care about the specific Context locale under which this\n", + " # query was made.\n", + " is_locale_special = False\n", + " special_locale_value = False\n", + "\n", + " # Assume we know from reading release notes that UnchangedTrait\n", + " # hasn't changed, so we can use v2 knowing that v1 is equivalent.\n", + " if v2.traits.example.UnchangedTrait.isImbuedTo(context.locale):\n", + " # UpdatedTrait changes between versions, but we know from\n", + " # release notes that the property we're interested in still\n", + " # exists semantically, it's just the name has changed.\n", + "\n", + " # Check if v2 of UpdatedTrait is imbued, and if so extract\n", + " # the property via its new name.\n", + " if v2.traits.example.UpdatedTrait.isImbuedTo(context.locale):\n", + " is_locale_special = True\n", + " special_locale_value = v2.traits.example.UpdatedTrait(\n", + " context.locale).getPropertyThatWasRenamed(defaultValue=special_locale_value)\n", + "\n", + " # If v2 of UpdatedTrait was not imbued, fall back to v1. If\n", + " # imbued, extract the property via its old name. If both v2\n", + " # and v1 were imbued for some reason, prefer v2.\n", + " elif v1.traits.example.UpdatedTrait.isImbuedTo(context.locale):\n", + " is_locale_special = True\n", + " special_locale_value = v1.traits.example.UpdatedTrait(\n", + " context.locale).getPropertyToRename(defaultValue=special_locale_value)\n", + "\n", + " for trait_set, policy_data in zip(traitSets, policy_datas):\n", + " if policyAccess is access.PolicyAccess.kRead:\n", + "\n", + " # Read is only supported when the locale is \"special\".\n", + " if not is_locale_special:\n", + " continue\n", + "\n", + " # Only sets with the v2 Added or v1 Removed trait are\n", + " # supported.\n", + " if not (v1.traits.example.RemovedTrait.kId in trait_set or\n", + " v2.traits.example.AddedTrait.kId in trait_set):\n", + " continue\n", + "\n", + " if special_locale_value is True:\n", + " # Since the locale's \"special\" value is set, we are\n", + " # capable of providing property values for either or\n", + " # both v1 and v2 of UpdatedTrait simultaneously.\n", + "\n", + " if v2.traits.example.UpdatedTrait.kId in trait_set:\n", + " v2.traits.example.UpdatedTrait.imbueTo(policy_data)\n", + "\n", + " if v1.traits.example.UpdatedTrait.kId in trait_set:\n", + " v1.traits.example.UpdatedTrait.imbueTo(policy_data)\n", + " else:\n", + " # Since the locale's \"special\" value is not set, we\n", + " # can only provide property values for either v1 or\n", + " # v2, but not both, of UpdatedTrait. We prefer v2.\n", + "\n", + " if (v2.traits.example.UpdatedTrait.kId in trait_set\n", + " and not v1.traits.example.UpdatedTrait.kId in trait_set):\n", + " v2.traits.example.UpdatedTrait.imbueTo(policy_data)\n", + " elif v1.traits.example.UpdatedTrait.kId in trait_set:\n", + " v1.traits.example.UpdatedTrait.imbueTo(policy_data)\n", + "\n", + " continue\n", + " else:\n", + " # Other access modes all the same.\n", + " if not is_locale_special:\n", + " continue\n", + "\n", + " # Prefer v2, ignoring v1 if it is set.\n", + "\n", + " if v2.traits.example.UpdatedTrait.kId in trait_set:\n", + " v2.traits.example.UpdatedTrait.imbueTo(policy_data)\n", + " elif v1.traits.example.UpdatedTrait.kId in trait_set:\n", + " v1.traits.example.UpdatedTrait.imbueTo(policy_data)\n", + "\n", + " return policy_datas\n", + "\n", + " def defaultEntityReference(\n", + " self, traitSets, defaultEntityAccess, context, hostSession, successCallback,\n", + " errorCallback):\n", + " for idx, trait_set in enumerate(traitSets):\n", + " is_an_unchanged = v2.traits.example.UnchangedTrait.kId in trait_set\n", + " is_an_updated = (v2.traits.example.UpdatedTrait.kId in trait_set or\n", + " v1.traits.example.UpdatedTrait.kId in trait_set)\n", + " is_a_removed = v1.traits.example.RemovedTrait.kId in trait_set\n", + " is_an_added = v2.traits.example.AddedTrait.kId in trait_set\n", + "\n", + " if is_an_unchanged and is_an_updated and is_a_removed:\n", + " # Only possible with v1 schema/trait\n", + " entity_ref = \"example://default/removed\"\n", + " elif is_an_unchanged and is_an_updated and is_an_added:\n", + " # Only possible with v2 schema/trait\n", + " entity_ref = \"example://default/added\"\n", + " elif is_an_unchanged and is_an_updated:\n", + " # v1 or v2 schemas\n", + " entity_ref = \"example://default\"\n", + " else:\n", + " # Any other unrecognized trait set\n", + " errorCallback(\n", + " idx, errors.BatchElementError(\n", + " errors.BatchElementError.ErrorCode.kInvalidTraitSet,\n", + " \"Entity trait set unrecognised\"))\n", + " continue\n", + "\n", + " # Not really important here, but for completeness handle all\n", + " # access modes.\n", + " if defaultEntityAccess is access.DefaultEntityAccess.kWrite:\n", + " entity_ref += \"/new\"\n", + " elif defaultEntityAccess is access.DefaultEntityAccess.kCreateRelated:\n", + " entity_ref += \"/child/new\"\n", + "\n", + " successCallback(idx, entity_ref)\n", + "\n", + " def entityTraits(\n", + " self,\n", + " entityRefs,\n", + " entityTraitsAccess,\n", + " context,\n", + " hostSession,\n", + " successCallback,\n", + " errorCallback):\n", + " for idx, entity_ref in enumerate(entityRefs):\n", + " # This manager only supports one entity ref.\n", + " if str(entity_ref) == an_entity_ref_str:\n", + " # Read access\n", + " if entityTraitsAccess == access.EntityTraitsAccess.kRead:\n", + " # We use the ExampleSpecification - a well-known\n", + " # trait set with all traits required to categorize\n", + " # an entity as an Example. No way to know what\n", + " # version the host would prefer. So prefer latest,\n", + " # v2.\n", + " successCallback(idx, v2.specifications.example.ExampleSpecification.kTraitSet)\n", + " else:\n", + " # Minimum required for publishing is a reduced set.\n", + " successCallback(\n", + " idx, v2.specifications.example.ExampleSpecification.kTraitSet - {\n", + " v2.traits.example.AddedTrait.kId})\n", + "\n", + " else:\n", + " errorCallback(\n", + " idx, errors.BatchElementError(\n", + " errors.BatchElementError.ErrorCode.kEntityResolutionError,\n", + " \"Entity doesn't exist\"))\n", + "\n", + " def resolve(\n", + " self,\n", + " entityRefs,\n", + " traitSet,\n", + " resolveAccess,\n", + " context,\n", + " hostSession,\n", + " successCallback,\n", + " errorCallback):\n", + " traits_datas = [TraitsData() for _ in entityRefs]\n", + "\n", + " for idx, (entity_ref, traits_data) in enumerate(zip(entityRefs, traits_datas)):\n", + " if str(entity_ref) == an_entity_ref_str:\n", + " if resolveAccess == access.ResolveAccess.kRead:\n", + " # Support either v1 or v2 (or both) for read.\n", + " if v2.traits.example.UpdatedTrait.kId in traitSet:\n", + " trait = v2.traits.example.UpdatedTrait(traits_data)\n", + " trait.setPropertyToKeep(\"value\")\n", + " trait.setPropertyThatWasRenamed(True)\n", + " trait.setPropertyThatWasAdded(123.456)\n", + " if v1.traits.example.UpdatedTrait.kId in traitSet:\n", + " trait = v1.traits.example.UpdatedTrait(traits_data)\n", + " trait.setPropertyToKeep(\"value\")\n", + " trait.setPropertyToRename(True)\n", + " trait.setPropertyToRemove(False)\n", + "\n", + " elif resolveAccess == access.ResolveAccess.kManagerDriven:\n", + " # Only support v2 for publishing workflows.\n", + " if v2.traits.example.UpdatedTrait.kId in traitSet:\n", + " trait = v2.traits.example.UpdatedTrait(traits_data)\n", + " trait.setPropertyThatWasAdded(456.789)\n", + " else:\n", + " # Similar for other entities.\n", + " if resolveAccess == access.ResolveAccess.kManagerDriven:\n", + " # Only support v2 for publishing workflows.\n", + " if v2.traits.example.UpdatedTrait.kId in traitSet:\n", + " trait = v2.traits.example.UpdatedTrait(traits_data)\n", + " trait.setPropertyThatWasAdded(789.123)\n", + " trait.setPropertyThatWasRenamed(True)\n", + "\n", + " successCallback(idx, traits_data)\n", + "\n", + " def getWithRelationship(\n", + " self,\n", + " entityRefs,\n", + " relationshipTraitsData,\n", + " resultTraitSet,\n", + " pageSize,\n", + " relationsAccess,\n", + " context,\n", + " hostSession,\n", + " successCallback,\n", + " errorCallback):\n", + "\n", + " # Parse out important aspects of the type of relationship.\n", + "\n", + " is_rel_unchanged = v2.traits.example.UnchangedTrait.isimbuedTo(\n", + " relationshipTraitsData)\n", + "\n", + " rel_v2_updated = v2.traits.example.UpdatedTrait(relationshipTraitsData)\n", + " rel_v1_updated = v1.traits.example.UpdatedTrait(relationshipTraitsData)\n", + " is_rel_updated = rel_v2_updated.isImbued() or rel_v1_updated.isImbued()\n", + "\n", + " important_property = rel_v2_updated.getPropertyThatWasRenamed(\n", + " defaultValue=rel_v1_updated.getPropertyToRename(defaultValue=False))\n", + "\n", + " is_a_child_relationship = is_rel_unchanged and is_rel_updated\n", + "\n", + " # Parse out important aspects of the type of expected result entity.\n", + "\n", + " result_type = \"none\"\n", + " if {v2.traits.example.AddedTrait.kId,\n", + " v2.traits.example.UnchangedTrait.kId}.issubset(resultTraitSet):\n", + " result_type = \"component\"\n", + " elif {v1.traits.example.RemovedTrait.kId,\n", + " v2.traits.example.UnchangedTrait.kId}.issubset(resultTraitSet):\n", + " result_type = \"element\"\n", + "\n", + " # Loop through input entities.\n", + "\n", + " for idx, entity_ref in enumerate(entityRefs):\n", + "\n", + " rels = []\n", + "\n", + " if relationsAccess is access.RelationsAccess.kRead:\n", + " if str(entity_ref) == an_entity_ref_str:\n", + " if is_a_child_relationship:\n", + " if result_type == \"component\":\n", + " rels.append(EntityReference(\"example://entity/component/1\"))\n", + " rels.append(EntityReference(\"example://entity/component/2\"))\n", + "\n", + " elif result_type == \"element\":\n", + " rels.append(EntityReference(\"example://entity/element/a\"))\n", + "\n", + " if result_type != \"none\" and important_property is True:\n", + " rels.append(EntityReference(\"example://entity/component/3/element/b\"))\n", + " else:\n", + " # Similar for other entity refs.\n", + " ...\n", + "\n", + " elif relationsAccess is access.RelationsAccess.kWrite:\n", + " if str(entity_ref) == an_entity_ref_str:\n", + " if is_a_child_relationship:\n", + " # Only respond for \"component\" (i.e. v2) relationships.\n", + " if result_type == \"component\":\n", + " rels.append(EntityReference(\"example://entity/component/1/edit\"))\n", + " rels.append(EntityReference(\"example://entity/component/2/edit\"))\n", + " else:\n", + " # Similar for other entity refs.\n", + " ...\n", + "\n", + " elif relationsAccess is access.RelationsAccess.kCreateRelated:\n", + " if str(entity_ref) == an_entity_ref_str:\n", + " if is_a_child_relationship:\n", + " # Only respond for \"component\" (i.e. v2) relationships.\n", + " if result_type == \"component\":\n", + " rels.append(EntityReference(\"example://entity/component/new\"))\n", + " else:\n", + " # Similar for other entity refs.\n", + " ...\n", + "\n", + " successCallback(idx, ExampleEntityReferencePagerInterface(pageSize, rels))\n", + "\n", + " def getWithRelationships(\n", + " self,\n", + " entityReference,\n", + " relationshipTraitsDatas,\n", + " resultTraitSet,\n", + " pageSize,\n", + " relationsAccess,\n", + " context,\n", + " hostSession,\n", + " successCallback,\n", + " errorCallback):\n", + " # Largely same as getWithRelationship, with outer loop changed.\n", + " ...\n", + "\n", + " def preflight(\n", + " self,\n", + " targetEntityRefs,\n", + " traitsDatas,\n", + " publishingAccess,\n", + " context,\n", + " hostSession,\n", + " successCallback,\n", + " errorCallback):\n", + "\n", + " for idx, (entity_ref, traits_data) in enumerate(zip(targetEntityRefs, traitsDatas)):\n", + " if str(entity_ref) != an_entity_ref_str:\n", + " errorCallback(\n", + " idx, errors.BatchElementError(\n", + " errors.BatchElementError.ErrorCode.kEntityAccessError,\n", + " \"Cannot publish to this entity\"))\n", + " continue\n", + "\n", + " # Categorise the data to publish\n", + " is_an_unchanged = v2.traits.example.UnchangedTrait.isImbuedTo(traits_data)\n", + " is_an_updated = (v2.traits.example.UpdatedTrait.isImbuedTo(traits_data) or\n", + " v1.traits.example.UpdatedTrait.isImbuedTo(traits_data))\n", + " is_a_removed = v1.traits.example.RemovedTrait.isImbuedTo(traits_data)\n", + " is_an_added = v2.traits.example.AddedTrait.isImbuedTo(traits_data)\n", + "\n", + " # Based on the given traits, categorize to pipeline-specific, \"type\"\n", + " is_an_item = is_an_added and is_an_updated\n", + " is_a_unit = is_a_removed and is_an_updated\n", + " is_an_ingredient = is_an_unchanged and is_an_updated\n", + "\n", + " # At least and only one, i.e. n-ary xor.\n", + " if int(is_an_item) + int(is_a_unit) + int(is_an_ingredient) != 1:\n", + " errorCallback(\n", + " idx, errors.BatchElementError(\n", + " errors.BatchElementError.ErrorCode.kInvalidPreflightHint,\n", + " \"Unsupported traits for publishing to this entity\"))\n", + " continue\n", + "\n", + " if is_an_item:\n", + " v2_updated_trait = v2.traits.example.UpdatedTrait(traits_data)\n", + " v1_updated_trait = v1.traits.example.UpdatedTrait(traits_data)\n", + "\n", + " if v2_updated_trait.isImbued():\n", + " specialisation = v2_updated_trait.getPropertyToKeep()\n", + " else: # at this point guaranteed that v1 is imbued.\n", + " specialisation = v1_updated_trait.getPropertyToKeep()\n", + "\n", + " successCallback(\n", + " idx, EntityReference(\n", + " f\"example://working_ref/item/{specialisation}/new\"))\n", + "\n", + " elif is_a_unit:\n", + " successCallback(idx, EntityReference(f\"example://working_ref/unit/new\"))\n", + "\n", + " elif is_an_ingredient:\n", + " successCallback(idx, EntityReference(f\"example://working_ref/ingredient/new\"))\n", + "\n", + " def register(\n", + " self,\n", + " targetEntityRefs,\n", + " entityTraitsDatas,\n", + " publishingAccess,\n", + " context,\n", + " hostSession,\n", + " successCallback,\n", + " errorCallback):\n", + " for idx, (entity_ref, traits_data) in enumerate(zip(targetEntityRefs, entityTraitsDatas)):\n", + " # Must provide a reference returned from `preflight`\n", + " if not str(entity_ref).startswith(\"example://working_ref/\"):\n", + " errorCallback(\n", + " idx, errors.BatchElementError(\n", + " errors.BatchElementError.ErrorCode.kEntityAccessError,\n", + " \"Cannot publish to this entity\"))\n", + " continue\n", + "\n", + " # Categorise the data to publish\n", + " is_an_unchanged = v2.traits.example.UnchangedTrait.isImbuedTo(traits_data)\n", + " is_an_updated = (v2.traits.example.UpdatedTrait.isImbuedTo(traits_data) or\n", + " v1.traits.example.UpdatedTrait.isImbuedTo(traits_data))\n", + " is_a_removed = v1.traits.example.RemovedTrait.isImbuedTo(traits_data)\n", + " is_an_added = v2.traits.example.AddedTrait.isImbuedTo(traits_data)\n", + "\n", + " # Based on the given traits, categorize to pipeline-specific, \"type\"\n", + " is_an_item = is_an_added and is_an_updated\n", + " is_a_unit = is_a_removed and is_an_updated\n", + " is_an_ingredient = is_an_unchanged and is_an_updated\n", + "\n", + " # At least and only one, i.e. n-ary xor.\n", + " if int(is_an_item) + int(is_a_unit) + int(is_an_ingredient) != 1:\n", + " errorCallback(\n", + " idx, errors.BatchElementError(\n", + " errors.BatchElementError.ErrorCode.kPreflightHintError,\n", + " \"Unsupported traits for publishing to this entity\"))\n", + " continue\n", + "\n", + " v2_updated_trait = v2.traits.example.UpdatedTrait(traits_data)\n", + " v1_updated_trait = v1.traits.example.UpdatedTrait(traits_data)\n", + " if is_an_item:\n", + " if v2_updated_trait.isImbued():\n", + " foo = v2_updated_trait.getPropertyThatWasRenamed()\n", + " bar = v2_updated_trait.getPropertyThatWasAdded()\n", + " do_backend_operation(ref=entity_ref, foo=foo, bar=bar, baz=None)\n", + " else: # at this point guaranteed that v1 is imbued.\n", + " foo = v1_updated_trait.getPropertyToRename()\n", + " baz = v1_updated_trait.getPropertyToRemove()\n", + " do_backend_operation(ref=entity_ref, foo=foo, bar=None, baz=baz)\n", + "\n", + " successCallback(idx, EntityReference(f\"example://item\"))\n", + "\n", + " elif is_a_unit:\n", + " if v2_updated_trait.isImbued():\n", + " foo = v2_updated_trait.getPropertyToKeep()\n", + " do_backend_operation(ref=entity_ref, foo=foo)\n", + " else: # at this point guaranteed that v1 is imbued.\n", + " foo = v1_updated_trait.getPropertyToKeep()\n", + " do_backend_operation(ref=entity_ref, foo=foo)\n", + "\n", + " successCallback(idx, EntityReference(f\"example://unit\"))\n", + "\n", + " elif is_an_ingredient:\n", + " if v2_updated_trait.isImbued():\n", + " baz = v2_updated_trait.getPropertyThatWasAdded()\n", + " do_backend_operation(ref=entity_ref, foo=None, baz=baz)\n", + " else: # at this point guaranteed that v1 is imbued.\n", + " foo = v1_updated_trait.getPropertyToRemove()\n", + " do_backend_operation(ref=entity_ref, foo=foo, baz=None)\n", + "\n", + " successCallback(idx, EntityReference(f\"example://ingredient\"))\n", + "\n", + "\n", + "class ExampleEntityReferencePagerInterface(EntityReferencePagerInterface):\n", + " def __init__(self, page_size, results):\n", + " self.__results = results\n", + " self.__idx = 0\n", + " self.__page_size = page_size\n", + " EntityReferencePagerInterface.__init__(self)\n", + "\n", + " def hasNext(self, _hostSession):\n", + " return self.__idx < len(self.__results)\n", + "\n", + " def get(self, _hostSession):\n", + " return self.__results[self.__idx:self.__idx + self.__page_size]\n", + "\n", + " def next(self, _hostSession):\n", + " self.__idx += self.__page_size\n", + "\n", + " def close(self, _hostSession):\n", + " pass\n", + "\n", + "\n", + "def do_backend_operation(**kwargs):\n", + " # Do some pipeline-specific backend operation.\n", + " pass\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-17T15:20:50.073149Z", + "start_time": "2024-04-17T15:20:49.992403Z" + } + }, + "id": "5bb5454ca1ae8b06", + "execution_count": 11 + }, + { + "cell_type": "markdown", + "source": [ + "### Host\n", + "\n", + "Next we investigate how a host application might interact with this manager. The following is modified from the generic republish workflow in `generic_republish.ipynb`. Once again, the semantics are nonsense, but hopefully the branching logic is instructive." + ], + "metadata": { + "collapsed": false + }, + "id": "864fe9a610fff83" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from openassetio.hostApi import Manager\n", + "\n", + "\n", + "# Boilerplate preamble.\n", + "manager = Manager(ExampleManagerInterface(), host_session)\n", + "context = manager.createContext()\n", + "an_entity_ref = manager.createEntityReference(an_entity_ref_str)\n", + "\n", + "# Configure the locale\n", + "\n", + "context.locale.addTraits(v2.specifications.example.ExampleSpecification.kTraitSet)\n", + "\n", + "# The minimum set of traits required to publish to this entity\n", + "# reference.\n", + "minimum_trait_set = manager.entityTraits(an_entity_ref, access.EntityTraitsAccess.kWrite, context)\n", + "\n", + "# Schema version to use for traits/specifications when creating new\n", + "# trait sets/data. Downgrade when manager seems to indicate it doesn't\n", + "# support the latest version.\n", + "preferred_schema_version = 2\n", + "\n", + "# Whatever the minimum trait set is, we know we're going to publish an\n", + "# Example.\n", + "desired_trait_set = minimum_trait_set | v2.specifications.example.ExampleSpecification.kTraitSet\n", + "\n", + "# Get the set of traits that have properties the manager can persist.\n", + "[policy_for_desired_traits] = manager.managementPolicy(\n", + " [desired_trait_set], access.PolicyAccess.kWrite, context)\n", + "\n", + "# Check if the policy contains a trait that we expect to persist, and\n", + "# attempt to fall back to v1 if the trait is not supported. This\n", + "# requires us to know that the UpdatedTrait is part of the\n", + "# ExampleSpecification (and it is the only trait that carries `resolve`able\n", + "# properties).\n", + "if not v2.traits.example.UpdatedTrait.isImbuedTo(policy_for_desired_traits):\n", + " # v2 didn't work, try v1.\n", + " desired_trait_set = minimum_trait_set | v1.specifications.example.ExampleSpecification.kTraitSet\n", + "\n", + " # Get the set of traits that have properties the manager can\n", + " # persist.\n", + " [policy_for_desired_traits] = manager.managementPolicy(\n", + " [desired_trait_set], access.PolicyAccess.kWrite, context)\n", + "\n", + " if not v1.traits.example.UpdatedTrait.isImbuedTo(policy_for_desired_traits):\n", + " # v1 didn't work either, bail.\n", + " raise Exception(f\"Cannot publish an Example to ref {an_entity_ref}\")\n", + "\n", + " # We know the manager doesn't support v2, so downgrade the schema\n", + " # version to use when constructing new trait sets/data. Note that\n", + " # there's no guarantee that the schema version we picked will work\n", + " # for every trait set - for example, the manager may have mixed\n", + " # trait support across schema versions.\n", + " preferred_schema_version = 1\n", + "\n", + "# Filter down the desired traits to only those that are supported.\n", + "trait_set_to_publish = desired_trait_set & policy_for_desired_traits.traitSet()\n", + "\n", + "# We want to keep (the minimum amount of) data from the previous\n", + "# version, except for the values we're going to provide.\n", + "if preferred_schema_version == 2:\n", + " trait_set_to_keep = trait_set_to_publish - v2.specifications.example.ExampleSpecification.kTraitSet\n", + "else:\n", + " trait_set_to_keep = trait_set_to_publish - v1.specifications.example.ExampleSpecification.kTraitSet\n", + "\n", + "# Get the properties that we wish to keep from the current version.\n", + "data_to_publish = manager.resolve(\n", + " an_entity_ref, trait_set_to_keep, access.ResolveAccess.kRead, context)\n", + "\n", + "# Any traits without properties, or where the manager cannot provide\n", + "# them, will be missing from the data. We still need to imbue those\n", + "# traits, so that manager knows what kind of entity we are publishing.\n", + "data_to_publish.addTraits(minimum_trait_set)\n", + "\n", + "# Get the manager's policy for dictating trait properties, i.e. which\n", + "# traits the manager can \"drive\" for us.\n", + "[policy_for_derived_traits] = manager.managementPolicy(\n", + " [trait_set_to_publish], access.PolicyAccess.kManagerDriven, context)\n", + "\n", + "# Check if the manager can derive a value for us.\n", + "if v2.traits.example.UpdatedTrait.isImbuedTo(policy_for_derived_traits):\n", + " # Imbue an empty trait, so that the manager is aware in `preflight`\n", + " # that we intend to publish this trait. We will ask the manager to\n", + " # fill in the value for us before calling `register`.\n", + " v2.traits.example.UpdatedTrait.imbueTo(data_to_publish)\n", + "elif v1.traits.example.UpdatedTrait.isImbuedTo(policy_for_derived_traits):\n", + " # Fall back to v1.\n", + " v1.traits.example.UpdatedTrait.imbueTo(data_to_publish)\n", + "else:\n", + " # If the manager doesn't want to provide a value for entities of\n", + " # this type, use a default.\n", + " if preferred_schema_version == 2:\n", + " v2.traits.example.UpdatedTrait(data_to_publish).setPropertyThatWasRenamed(True)\n", + " else:\n", + " v1.traits.example.UpdatedTrait(data_to_publish).setPropertyToRename(True)\n", + "\n", + "# We can now successfully begin the publishing process.\n", + "working_ref = manager.preflight(\n", + " an_entity_ref, data_to_publish, access.PublishingAccess.kWrite, context)\n", + "\n", + "# Check if the manager can provide a value to us.\n", + "# First try v2.\n", + "if v2.traits.example.UpdatedTrait.kId in policy_for_derived_traits.traitSet():\n", + " derived_data = manager.resolve(\n", + " working_ref, {v2.traits.example.UpdatedTrait.kId}, access.ResolveAccess.kManagerDriven,\n", + " context)\n", + "\n", + " v2.traits.example.UpdatedTrait(data_to_publish).setPropertyThatWasRenamed(\n", + " v2.traits.example.UpdatedTrait(derived_data).getPropertyThatWasRenamed())\n", + "\n", + "# Fall back to v1.\n", + "elif v1.traits.example.UpdatedTrait.kId in policy_for_derived_traits.traitSet():\n", + " derived_data = manager.resolve(\n", + " working_ref, {v1.traits.example.UpdatedTrait.kId}, access.ResolveAccess.kManagerDriven,\n", + " context)\n", + "\n", + " v1.traits.example.UpdatedTrait(data_to_publish).setPropertyToRename(\n", + " v1.traits.example.UpdatedTrait(derived_data).getPropertyToRename())\n", + "\n", + "# [Do some work to write the new file...]\n", + "\n", + "# We can now finally publish\n", + "updated_ref = manager.register(\n", + " working_ref, data_to_publish, access.PublishingAccess.kWrite, context)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-17T13:35:09.638280Z", + "start_time": "2024-04-17T13:35:09.624587Z" + } + }, + "id": "b8e12c55d88bda27", + "execution_count": 8 + }, + { + "cell_type": "markdown", + "source": [ + "## Conclusion\n", + "\n", + "The above explorations have shown that working with versioned traits is entirely possible using the existing API.\n", + "\n", + "There are a few cases where the version of a trait is unimportant, i.e. where the properties are not required since we only wish to use the traits to categorize an entity/relationship/policy/locale. Those cases may warrant API changes to reduce boilerplate. In particular, Specification view classes are currently unsuitable for this use-case. However, this boilerplate does not _prevent_ workflows. Specifications can be used as documentation to help authors construct their own trait set detection logic. So the addition of utility functions is not critical to working with versioned traits.\n", + "\n", + "Conversely, when constructing a trait set or data, a schema version must be chosen by the host/manager. In the case of hosts adapting to managers, the `managementPolicy` API method provides a negotiation mechanism. Otherwise, the host/manager must simply choose a preferred schema version. It is in these circumstances, where new trait data is being created, that Specification view classes are particularly useful.\n", + "\n", + "It seems clear that dealing with mixed trait versions adds a lot of branching logic that could be hard to follow and maintain. A tempting solution is to expose the preferred schema version as a queryable value, so that branching can be performed at a higher level. However, as discussed in [DR023](https://github.com/OpenAssetIO/OpenAssetIO/blob/main/doc/decisions/DR023-Versioning-traits-and-specifications-method.md), this precludes many important workflows, since the ultimate provenance of trait data is unknown in the general case. For example, the manager may combine old data from a database with newly generated data; or the host may load an old project file holding trait data of a previous schema version; or multiple components of a system, each working with a different schema version, may collaborate to produce a trait set/data." + ], + "metadata": { + "collapsed": false + }, + "id": "257d865837cecdad" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 841c532824d86dd4cd65ead06011b72695e53c3b Mon Sep 17 00:00:00 2001 From: David Feltell Date: Mon, 22 Apr 2024 17:51:35 +0100 Subject: [PATCH 2/4] squash/drop: Add support for `_vX`-suffixed view classes Signed-off-by: David Feltell --- examples/working_with_trait_versions.ipynb | 182 ++++++++++++--------- 1 file changed, 109 insertions(+), 73 deletions(-) diff --git a/examples/working_with_trait_versions.ipynb b/examples/working_with_trait_versions.ipynb index 80fefc9..8af4fc7 100644 --- a/examples/working_with_trait_versions.ipynb +++ b/examples/working_with_trait_versions.ipynb @@ -91,7 +91,11 @@ "\n", "We imagine an industry where there are only 4 traits, `AddedTrait`, `RemovedTrait`, `UnchangedTrait` and `UpdatedTrait`, which are used across entities, relationships, policies and locales. The traits themselves have no meaning, they are named purely to give a hint as to how they change in subsequent trait schema versions.\n", "\n", - "Within a subpackage, all traits within that subpackage's schema version are included, with unchanged traits being duplicated. Duplication may be replaced with aliasing in a future update of `traitgen`." + "Within a subpackage, all traits within that subpackage's schema version are included, with unchanged traits being duplicated. Duplication may be replaced with aliasing in a future update of `traitgen`.\n", + "\n", + "The current and any older versions of a trait are also represented under a subpackage, with a version suffix on the class name. The class name without a version suffix is an alias to the latest version available under that schema version.\n", + "\n", + "Traits can be removed from a schema, but will still exist under the schema subpackage if the package was generated to include older schema versions." ], "metadata": { "collapsed": false @@ -105,7 +109,13 @@ "assert v2.traits.example.UnchangedTrait is not v1.traits.example.UnchangedTrait\n", "assert v2.traits.example.UpdatedTrait is not v1.traits.example.UpdatedTrait\n", "\n", - "assert \"RemovedTrait\" not in v2.traits.example.__dict__\n", + "assert v2.traits.example.UpdatedTrait is v2.traits.example.UpdatedTrait_v2\n", + "assert v2.traits.example.UpdatedTrait is not v2.traits.example.UpdatedTrait_v1\n", + "assert \"UpdatedTrait_v2\" not in v1.traits.example.__dict__\n", + "\n", + "# RemovedTrait is missing from v2, but is still added to the v2\n", + "# subpackage to aid backward compatibility.\n", + "assert \"RemovedTrait\" in v2.traits.example.__dict__\n", "assert \"RemovedTrait\" in v1.traits.example.__dict__\n", "\n", "assert \"AddedTrait\" in v2.traits.example.__dict__\n", @@ -197,6 +207,23 @@ "id": "a147006dd4b9b453", "execution_count": 4 }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "However, a workaround to this is to use the fallback versioned trait view classes in the newer schema subpackage", + "id": "c5d303a5d19954b4" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "assert v2.traits.example.UpdatedTrait_v1.isImbuedTo(v1_data) is True\n", + "assert v2.traits.example.UpdatedTrait_v2.isImbuedTo(v2_data) is True" + ], + "id": "74707caa0ec8f1f6" + }, { "cell_type": "markdown", "source": [ @@ -312,6 +339,7 @@ "from openassetio.managerApi import ManagerInterface, EntityReferencePagerInterface\n", "from openassetio import errors, access, EntityReference\n", "\n", + "from resources.working_with_trait_versions.openassetio_example.v2 import traits\n", "\n", "an_entity_ref_str = \"example://entity\"\n", "\n", @@ -349,14 +377,14 @@ "\n", " # Assume we know from reading release notes that UnchangedTrait\n", " # hasn't changed, so we can use v2 knowing that v1 is equivalent.\n", - " if v2.traits.example.UnchangedTrait.isImbuedTo(context.locale):\n", + " if traits.example.UnchangedTrait.isImbuedTo(context.locale):\n", " # UpdatedTrait changes between versions, but we know from\n", " # release notes that the property we're interested in still\n", " # exists semantically, it's just the name has changed.\n", "\n", " # Check if v2 of UpdatedTrait is imbued, and if so extract\n", " # the property via its new name.\n", - " if v2.traits.example.UpdatedTrait.isImbuedTo(context.locale):\n", + " if traits.example.UpdatedTrait_v2.isImbuedTo(context.locale):\n", " is_locale_special = True\n", " special_locale_value = v2.traits.example.UpdatedTrait(\n", " context.locale).getPropertyThatWasRenamed(defaultValue=special_locale_value)\n", @@ -364,9 +392,9 @@ " # If v2 of UpdatedTrait was not imbued, fall back to v1. If\n", " # imbued, extract the property via its old name. If both v2\n", " # and v1 were imbued for some reason, prefer v2.\n", - " elif v1.traits.example.UpdatedTrait.isImbuedTo(context.locale):\n", + " elif traits.example.UpdatedTrait_v1.isImbuedTo(context.locale):\n", " is_locale_special = True\n", - " special_locale_value = v1.traits.example.UpdatedTrait(\n", + " special_locale_value = traits.example.UpdatedTrait_v1(\n", " context.locale).getPropertyToRename(defaultValue=special_locale_value)\n", "\n", " for trait_set, policy_data in zip(traitSets, policy_datas):\n", @@ -378,8 +406,8 @@ "\n", " # Only sets with the v2 Added or v1 Removed trait are\n", " # supported.\n", - " if not (v1.traits.example.RemovedTrait.kId in trait_set or\n", - " v2.traits.example.AddedTrait.kId in trait_set):\n", + " if not (traits.example.RemovedTrait.kId in trait_set or\n", + " traits.example.AddedTrait.kId in trait_set):\n", " continue\n", "\n", " if special_locale_value is True:\n", @@ -387,21 +415,21 @@ " # capable of providing property values for either or\n", " # both v1 and v2 of UpdatedTrait simultaneously.\n", "\n", - " if v2.traits.example.UpdatedTrait.kId in trait_set:\n", - " v2.traits.example.UpdatedTrait.imbueTo(policy_data)\n", + " if traits.example.UpdatedTrait_v2.kId in trait_set:\n", + " traits.example.UpdatedTrait_v2.imbueTo(policy_data)\n", "\n", - " if v1.traits.example.UpdatedTrait.kId in trait_set:\n", - " v1.traits.example.UpdatedTrait.imbueTo(policy_data)\n", + " if traits.example.UpdatedTrait_v1.kId in trait_set:\n", + " traits.example.UpdatedTrait_v1.imbueTo(policy_data)\n", " else:\n", " # Since the locale's \"special\" value is not set, we\n", " # can only provide property values for either v1 or\n", " # v2, but not both, of UpdatedTrait. We prefer v2.\n", "\n", - " if (v2.traits.example.UpdatedTrait.kId in trait_set\n", - " and not v1.traits.example.UpdatedTrait.kId in trait_set):\n", - " v2.traits.example.UpdatedTrait.imbueTo(policy_data)\n", - " elif v1.traits.example.UpdatedTrait.kId in trait_set:\n", - " v1.traits.example.UpdatedTrait.imbueTo(policy_data)\n", + " if (traits.example.UpdatedTrait_v2.kId in trait_set\n", + " and not traits.example.UpdatedTrait_v1.kId in trait_set):\n", + " traits.example.UpdatedTrait_v2.imbueTo(policy_data)\n", + " elif traits.example.UpdatedTrait_v1.kId in trait_set:\n", + " traits.example.UpdatedTrait_v1.imbueTo(policy_data)\n", "\n", " continue\n", " else:\n", @@ -411,10 +439,10 @@ "\n", " # Prefer v2, ignoring v1 if it is set.\n", "\n", - " if v2.traits.example.UpdatedTrait.kId in trait_set:\n", - " v2.traits.example.UpdatedTrait.imbueTo(policy_data)\n", - " elif v1.traits.example.UpdatedTrait.kId in trait_set:\n", - " v1.traits.example.UpdatedTrait.imbueTo(policy_data)\n", + " if traits.example.UpdatedTrait_v2.kId in trait_set:\n", + " traits.example.UpdatedTrait_v2.imbueTo(policy_data)\n", + " elif traits.example.UpdatedTrait_v1.kId in trait_set:\n", + " traits.example.UpdatedTrait_v1.imbueTo(policy_data)\n", "\n", " return policy_datas\n", "\n", @@ -422,11 +450,11 @@ " self, traitSets, defaultEntityAccess, context, hostSession, successCallback,\n", " errorCallback):\n", " for idx, trait_set in enumerate(traitSets):\n", - " is_an_unchanged = v2.traits.example.UnchangedTrait.kId in trait_set\n", - " is_an_updated = (v2.traits.example.UpdatedTrait.kId in trait_set or\n", - " v1.traits.example.UpdatedTrait.kId in trait_set)\n", - " is_a_removed = v1.traits.example.RemovedTrait.kId in trait_set\n", - " is_an_added = v2.traits.example.AddedTrait.kId in trait_set\n", + " is_an_unchanged = traits.example.UnchangedTrait.kId in trait_set\n", + " is_an_updated = (traits.example.UpdatedTrait_v2.kId in trait_set or\n", + " traits.example.UpdatedTrait_v1.kId in trait_set)\n", + " is_a_removed = traits.example.RemovedTrait.kId in trait_set\n", + " is_an_added = traits.example.AddedTrait.kId in trait_set\n", "\n", " if is_an_unchanged and is_an_updated and is_a_removed:\n", " # Only possible with v1 schema/trait\n", @@ -477,7 +505,7 @@ " # Minimum required for publishing is a reduced set.\n", " successCallback(\n", " idx, v2.specifications.example.ExampleSpecification.kTraitSet - {\n", - " v2.traits.example.AddedTrait.kId})\n", + " traits.example.AddedTrait.kId})\n", "\n", " else:\n", " errorCallback(\n", @@ -500,28 +528,28 @@ " if str(entity_ref) == an_entity_ref_str:\n", " if resolveAccess == access.ResolveAccess.kRead:\n", " # Support either v1 or v2 (or both) for read.\n", - " if v2.traits.example.UpdatedTrait.kId in traitSet:\n", - " trait = v2.traits.example.UpdatedTrait(traits_data)\n", + " if traits.example.UpdatedTrait_v2.kId in traitSet:\n", + " trait = traits.example.UpdatedTrait_v2(traits_data)\n", " trait.setPropertyToKeep(\"value\")\n", " trait.setPropertyThatWasRenamed(True)\n", " trait.setPropertyThatWasAdded(123.456)\n", - " if v1.traits.example.UpdatedTrait.kId in traitSet:\n", - " trait = v1.traits.example.UpdatedTrait(traits_data)\n", + " if traits.example.UpdatedTrait_v1.kId in traitSet:\n", + " trait = traits.example.UpdatedTrait_v1(traits_data)\n", " trait.setPropertyToKeep(\"value\")\n", " trait.setPropertyToRename(True)\n", " trait.setPropertyToRemove(False)\n", "\n", " elif resolveAccess == access.ResolveAccess.kManagerDriven:\n", " # Only support v2 for publishing workflows.\n", - " if v2.traits.example.UpdatedTrait.kId in traitSet:\n", - " trait = v2.traits.example.UpdatedTrait(traits_data)\n", + " if traits.example.UpdatedTrait_v2.kId in traitSet:\n", + " trait = traits.example.UpdatedTrait_v1(traits_data)\n", " trait.setPropertyThatWasAdded(456.789)\n", " else:\n", " # Similar for other entities.\n", " if resolveAccess == access.ResolveAccess.kManagerDriven:\n", - " # Only support v2 for publishing workflows.\n", - " if v2.traits.example.UpdatedTrait.kId in traitSet:\n", - " trait = v2.traits.example.UpdatedTrait(traits_data)\n", + " # Only support latest version (v2) for publishing workflows.\n", + " if traits.example.UpdatedTrait.kId in traitSet:\n", + " trait = traits.example.UpdatedTrait(traits_data)\n", " trait.setPropertyThatWasAdded(789.123)\n", " trait.setPropertyThatWasRenamed(True)\n", "\n", @@ -541,11 +569,11 @@ "\n", " # Parse out important aspects of the type of relationship.\n", "\n", - " is_rel_unchanged = v2.traits.example.UnchangedTrait.isimbuedTo(\n", + " is_rel_unchanged = traits.example.UnchangedTrait.isimbuedTo(\n", " relationshipTraitsData)\n", "\n", - " rel_v2_updated = v2.traits.example.UpdatedTrait(relationshipTraitsData)\n", - " rel_v1_updated = v1.traits.example.UpdatedTrait(relationshipTraitsData)\n", + " rel_v2_updated = traits.example.UpdatedTrait_v2(relationshipTraitsData)\n", + " rel_v1_updated = traits.example.UpdatedTrait_v1(relationshipTraitsData)\n", " is_rel_updated = rel_v2_updated.isImbued() or rel_v1_updated.isImbued()\n", "\n", " important_property = rel_v2_updated.getPropertyThatWasRenamed(\n", @@ -556,11 +584,11 @@ " # Parse out important aspects of the type of expected result entity.\n", "\n", " result_type = \"none\"\n", - " if {v2.traits.example.AddedTrait.kId,\n", - " v2.traits.example.UnchangedTrait.kId}.issubset(resultTraitSet):\n", + " if {traits.example.AddedTrait.kId,\n", + " traits.example.UnchangedTrait.kId}.issubset(resultTraitSet):\n", " result_type = \"component\"\n", - " elif {v1.traits.example.RemovedTrait.kId,\n", - " v2.traits.example.UnchangedTrait.kId}.issubset(resultTraitSet):\n", + " elif {traits.example.RemovedTrait.kId,\n", + " traits.example.UnchangedTrait.kId}.issubset(resultTraitSet):\n", " result_type = \"element\"\n", "\n", " # Loop through input entities.\n", @@ -641,11 +669,11 @@ " continue\n", "\n", " # Categorise the data to publish\n", - " is_an_unchanged = v2.traits.example.UnchangedTrait.isImbuedTo(traits_data)\n", - " is_an_updated = (v2.traits.example.UpdatedTrait.isImbuedTo(traits_data) or\n", - " v1.traits.example.UpdatedTrait.isImbuedTo(traits_data))\n", - " is_a_removed = v1.traits.example.RemovedTrait.isImbuedTo(traits_data)\n", - " is_an_added = v2.traits.example.AddedTrait.isImbuedTo(traits_data)\n", + " is_an_unchanged = traits.example.UnchangedTrait.isImbuedTo(traits_data)\n", + " is_an_updated = (traits.example.UpdatedTrait_v2.isImbuedTo(traits_data) or\n", + " traits.example.UpdatedTrait_v1.isImbuedTo(traits_data))\n", + " is_a_removed = traits.example.RemovedTrait.isImbuedTo(traits_data)\n", + " is_an_added = traits.example.AddedTrait.isImbuedTo(traits_data)\n", "\n", " # Based on the given traits, categorize to pipeline-specific, \"type\"\n", " is_an_item = is_an_added and is_an_updated\n", @@ -661,8 +689,8 @@ " continue\n", "\n", " if is_an_item:\n", - " v2_updated_trait = v2.traits.example.UpdatedTrait(traits_data)\n", - " v1_updated_trait = v1.traits.example.UpdatedTrait(traits_data)\n", + " v2_updated_trait = traits.example.UpdatedTrait_v2(traits_data)\n", + " v1_updated_trait = traits.example.UpdatedTrait_v1(traits_data)\n", "\n", " if v2_updated_trait.isImbued():\n", " specialisation = v2_updated_trait.getPropertyToKeep()\n", @@ -698,11 +726,11 @@ " continue\n", "\n", " # Categorise the data to publish\n", - " is_an_unchanged = v2.traits.example.UnchangedTrait.isImbuedTo(traits_data)\n", - " is_an_updated = (v2.traits.example.UpdatedTrait.isImbuedTo(traits_data) or\n", - " v1.traits.example.UpdatedTrait.isImbuedTo(traits_data))\n", - " is_a_removed = v1.traits.example.RemovedTrait.isImbuedTo(traits_data)\n", - " is_an_added = v2.traits.example.AddedTrait.isImbuedTo(traits_data)\n", + " is_an_unchanged = traits.example.UnchangedTrait.isImbuedTo(traits_data)\n", + " is_an_updated = (traits.example.UpdatedTrait_v2.isImbuedTo(traits_data) or\n", + " traits.example.UpdatedTrait_v1.isImbuedTo(traits_data))\n", + " is_a_removed = traits.example.RemovedTrait.isImbuedTo(traits_data)\n", + " is_an_added = traits.example.AddedTrait.isImbuedTo(traits_data)\n", "\n", " # Based on the given traits, categorize to pipeline-specific, \"type\"\n", " is_an_item = is_an_added and is_an_updated\n", @@ -717,8 +745,8 @@ " \"Unsupported traits for publishing to this entity\"))\n", " continue\n", "\n", - " v2_updated_trait = v2.traits.example.UpdatedTrait(traits_data)\n", - " v1_updated_trait = v1.traits.example.UpdatedTrait(traits_data)\n", + " v2_updated_trait = traits.example.UpdatedTrait_v2(traits_data)\n", + " v1_updated_trait = traits.example.UpdatedTrait_v1(traits_data)\n", " if is_an_item:\n", " if v2_updated_trait.isImbued():\n", " foo = v2_updated_trait.getPropertyThatWasRenamed()\n", @@ -786,6 +814,14 @@ "id": "5bb5454ca1ae8b06", "execution_count": 11 }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "", + "id": "64a4f3ecdfba5690" + }, { "cell_type": "markdown", "source": [ @@ -836,7 +872,7 @@ "# requires us to know that the UpdatedTrait is part of the\n", "# ExampleSpecification (and it is the only trait that carries `resolve`able\n", "# properties).\n", - "if not v2.traits.example.UpdatedTrait.isImbuedTo(policy_for_desired_traits):\n", + "if not traits.example.UpdatedTrait_v2.isImbuedTo(policy_for_desired_traits):\n", " # v2 didn't work, try v1.\n", " desired_trait_set = minimum_trait_set | v1.specifications.example.ExampleSpecification.kTraitSet\n", "\n", @@ -845,7 +881,7 @@ " [policy_for_desired_traits] = manager.managementPolicy(\n", " [desired_trait_set], access.PolicyAccess.kWrite, context)\n", "\n", - " if not v1.traits.example.UpdatedTrait.isImbuedTo(policy_for_desired_traits):\n", + " if not traits.example.UpdatedTrait_v1.isImbuedTo(policy_for_desired_traits):\n", " # v1 didn't work either, bail.\n", " raise Exception(f\"Cannot publish an Example to ref {an_entity_ref}\")\n", "\n", @@ -881,21 +917,21 @@ " [trait_set_to_publish], access.PolicyAccess.kManagerDriven, context)\n", "\n", "# Check if the manager can derive a value for us.\n", - "if v2.traits.example.UpdatedTrait.isImbuedTo(policy_for_derived_traits):\n", + "if traits.example.UpdatedTrait_v2.isImbuedTo(policy_for_derived_traits):\n", " # Imbue an empty trait, so that the manager is aware in `preflight`\n", " # that we intend to publish this trait. We will ask the manager to\n", " # fill in the value for us before calling `register`.\n", - " v2.traits.example.UpdatedTrait.imbueTo(data_to_publish)\n", - "elif v1.traits.example.UpdatedTrait.isImbuedTo(policy_for_derived_traits):\n", + " traits.example.UpdatedTrait_v2.imbueTo(data_to_publish)\n", + "elif traits.example.UpdatedTrait_v1.isImbuedTo(policy_for_derived_traits):\n", " # Fall back to v1.\n", - " v1.traits.example.UpdatedTrait.imbueTo(data_to_publish)\n", + " traits.example.UpdatedTrait_v1.imbueTo(data_to_publish)\n", "else:\n", " # If the manager doesn't want to provide a value for entities of\n", " # this type, use a default.\n", " if preferred_schema_version == 2:\n", - " v2.traits.example.UpdatedTrait(data_to_publish).setPropertyThatWasRenamed(True)\n", + " traits.example.UpdatedTrait_v2(data_to_publish).setPropertyThatWasRenamed(True)\n", " else:\n", - " v1.traits.example.UpdatedTrait(data_to_publish).setPropertyToRename(True)\n", + " traits.example.UpdatedTrait_v1(data_to_publish).setPropertyToRename(True)\n", "\n", "# We can now successfully begin the publishing process.\n", "working_ref = manager.preflight(\n", @@ -903,22 +939,22 @@ "\n", "# Check if the manager can provide a value to us.\n", "# First try v2.\n", - "if v2.traits.example.UpdatedTrait.kId in policy_for_derived_traits.traitSet():\n", + "if traits.example.UpdatedTrait_v2.kId in policy_for_derived_traits.traitSet():\n", " derived_data = manager.resolve(\n", - " working_ref, {v2.traits.example.UpdatedTrait.kId}, access.ResolveAccess.kManagerDriven,\n", + " working_ref, {traits.example.UpdatedTrait_v2.kId}, access.ResolveAccess.kManagerDriven,\n", " context)\n", "\n", - " v2.traits.example.UpdatedTrait(data_to_publish).setPropertyThatWasRenamed(\n", - " v2.traits.example.UpdatedTrait(derived_data).getPropertyThatWasRenamed())\n", + " traits.example.UpdatedTrait_v2(data_to_publish).setPropertyThatWasRenamed(\n", + " traits.example.UpdatedTrait_v2(derived_data).getPropertyThatWasRenamed())\n", "\n", "# Fall back to v1.\n", - "elif v1.traits.example.UpdatedTrait.kId in policy_for_derived_traits.traitSet():\n", + "elif traits.example.UpdatedTrait_v1.kId in policy_for_derived_traits.traitSet():\n", " derived_data = manager.resolve(\n", - " working_ref, {v1.traits.example.UpdatedTrait.kId}, access.ResolveAccess.kManagerDriven,\n", + " working_ref, {traits.example.UpdatedTrait_v1.kId}, access.ResolveAccess.kManagerDriven,\n", " context)\n", "\n", - " v1.traits.example.UpdatedTrait(data_to_publish).setPropertyToRename(\n", - " v1.traits.example.UpdatedTrait(derived_data).getPropertyToRename())\n", + " traits.example.UpdatedTrait_v1(data_to_publish).setPropertyToRename(\n", + " traits.example.UpdatedTrait_v1(derived_data).getPropertyToRename())\n", "\n", "# [Do some work to write the new file...]\n", "\n", From 6abf53e406ed518aef3179a4b93231305cc64f1f Mon Sep 17 00:00:00 2001 From: David Feltell Date: Tue, 7 May 2024 13:07:42 +0100 Subject: [PATCH 3/4] squash/drop: Remove top-level schema namespace Signed-off-by: David Feltell --- .../working_with_trait_versions/example.yml | 95 ++++++ .../openassetio_example/__init__.py | 3 +- .../{v1 => }/specifications/__init__.py | 0 .../specifications/example.py | 140 ++++++++ .../{v1 => }/traits/__init__.py | 0 .../{v1 => }/traits/example.py | 151 ++++++++- .../openassetio_example/v1/__init__.py | 8 - .../v1/specifications/example.py | 77 ----- .../openassetio_example/v2/__init__.py | 8 - .../v2/specifications/__init__.py | 7 - .../v2/specifications/example.py | 77 ----- .../openassetio_example/v2/traits/__init__.py | 7 - .../openassetio_example/v2/traits/example.py | 257 -------------- .../working_with_trait_versions/v1.yml | 62 ---- .../working_with_trait_versions/v2.yml | 61 ---- examples/working_with_trait_versions.ipynb | 319 +++++++----------- 16 files changed, 506 insertions(+), 766 deletions(-) create mode 100644 examples/resources/working_with_trait_versions/example.yml rename examples/resources/working_with_trait_versions/openassetio_example/{v1 => }/specifications/__init__.py (100%) create mode 100644 examples/resources/working_with_trait_versions/openassetio_example/specifications/example.py rename examples/resources/working_with_trait_versions/openassetio_example/{v1 => }/traits/__init__.py (100%) rename examples/resources/working_with_trait_versions/openassetio_example/{v1 => }/traits/example.py (60%) delete mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v1/__init__.py delete mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/example.py delete mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v2/__init__.py delete mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/__init__.py delete mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/example.py delete mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v2/traits/__init__.py delete mode 100644 examples/resources/working_with_trait_versions/openassetio_example/v2/traits/example.py delete mode 100644 examples/resources/working_with_trait_versions/v1.yml delete mode 100644 examples/resources/working_with_trait_versions/v2.yml diff --git a/examples/resources/working_with_trait_versions/example.yml b/examples/resources/working_with_trait_versions/example.yml new file mode 100644 index 0000000..0ea2e5a --- /dev/null +++ b/examples/resources/working_with_trait_versions/example.yml @@ -0,0 +1,95 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/OpenAssetIO/OpenAssetIO-TraitGen/main/python/openassetio_traitgen/schema.json +# yamllint disable-line rule:document-start +package: openassetio-example +description: An example traits schema + +traits: + example: + description: Example namespace + members: + + Deprecated: + deprecated: true + versions: + 1: + description: An example. + usage: + - entity + - locale + - relationship + + Added: + versions: + 1: + description: An example. + usage: + - entity + - locale + - relationship + + Updated: + versions: + 1: + description: An example. + usage: + - entity + - locale + - relationship + properties: + propertyToKeep: + type: string + description: A property that is unchanged between versions. + propertyToRename: + type: boolean + description: > + A property that has an inappropriate name and should be renamed in the + next version. + propertyToRemove: + type: boolean + description: A defunct property that should be removed in the next version. + 2: + description: An example. + usage: + - entity + - locale + - relationship + properties: + propertyToKeep: + type: string + description: A property that is unchanged between versions. + propertyThatWasRenamed: + type: boolean + description: A property that has been renamed. + propertyThatWasAdded: + type: float + description: A new property added in the latest version. + +specifications: + example: + description: Test specifications. + members: + + Example: + versions: + 1: + description: An example. + usage: + - entity + traitSet: + - namespace: example + name: Added + version: 1 + - namespace: example + name: Updated + version: 1 + 2: + description: An example. + usage: + - entity + traitSet: + - namespace: example + name: Added + version: 1 + - namespace: example + name: Updated + version: 2 diff --git a/examples/resources/working_with_trait_versions/openassetio_example/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/__init__.py index 0b1f0d8..a755489 100644 --- a/examples/resources/working_with_trait_versions/openassetio_example/__init__.py +++ b/examples/resources/working_with_trait_versions/openassetio_example/__init__.py @@ -4,4 +4,5 @@ # WARNING: This file is auto-generated by openassetio-traitgen, do not edit. -from .v2 import * +from . import traits +from . import specifications diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/specifications/__init__.py similarity index 100% rename from examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/__init__.py rename to examples/resources/working_with_trait_versions/openassetio_example/specifications/__init__.py diff --git a/examples/resources/working_with_trait_versions/openassetio_example/specifications/example.py b/examples/resources/working_with_trait_versions/openassetio_example/specifications/example.py new file mode 100644 index 0000000..4c6a068 --- /dev/null +++ b/examples/resources/working_with_trait_versions/openassetio_example/specifications/example.py @@ -0,0 +1,140 @@ + +""" +Specification definitions in the 'example' namespace. + +Test specifications. +""" + +# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. + +from openassetio.trait import TraitsData + + +from .. import traits + + +class ExampleSpecification_v2: + """ + An example. + Usage: entity + """ + kTraitSet = { + # 'openassetio-example:example.Deprecated' + traits.example.AddedTrait_v1.kId, + # 'openassetio-example:example.Updated' + traits.example.UpdatedTrait_v2.kId, + + } + + def __init__(self, traitsData): + """ + Constructs the specification as a view on the supplied + shared @fqref{TraitsData} "TraitsData" instance. + + @param traitsData @fqref{TraitsData} "TraitsData" + + @warning Specifications are always a view on the supplied data, + which is held by reference. Any changes made to the data will be + visible to any other specifications or @ref trait "traits" that + wrap the same TraitsData instance. + """ + if not isinstance(traitsData, TraitsData): + raise TypeError("Specifications must be constructed with a TraitsData instance") + self.__data = traitsData + + def traitsData(self): + """ + Returns the underlying (shared) @fqref{TraitsData} "TraitsData" + instance held by this specification. + """ + return self.__data + + @classmethod + def create(cls): + """ + Returns a new instance of the Specification, holding a new + @fqref{TraitsData} "TraitsData" instance, pre-populated with all + of the specifications traits. + """ + data = TraitsData(cls.kTraitSet) + return cls(data) + + def deprectatedTrait(self): + """ + Returns the view for the 'openassetio-example:example.Unchanged' trait wrapped around + the data held in this instance. + """ + return traits.example.DeprecatedTrait_v1(self.traitsData()) + + def updatedTrait(self): + """ + Returns the view for the 'openassetio-example:example.Updated' trait wrapped around + the data held in this instance. + """ + return traits.example.UpdatedTrait_v2(self.traitsData()) + + +# Alias for latest version. +ExampleSpecification = ExampleSpecification_v2 + + +class ExampleSpecification_v1: + """ + An example. + Usage: entity + """ + kTraitSet = { + # 'openassetio-example:example.Deprecated' + traits.example.AddedTrait_v1.kId, + # 'openassetio-example:example.Updated' + traits.example.UpdatedTrait_v1.kId, + + } + + def __init__(self, traitsData): + """ + Constructs the specification as a view on the supplied + shared @fqref{TraitsData} "TraitsData" instance. + + @param traitsData @fqref{TraitsData} "TraitsData" + + @warning Specifications are always a view on the supplied data, + which is held by reference. Any changes made to the data will be + visible to any other specifications or @ref trait "traits" that + wrap the same TraitsData instance. + """ + if not isinstance(traitsData, TraitsData): + raise TypeError("Specifications must be constructed with a TraitsData instance") + self.__data = traitsData + + def traitsData(self): + """ + Returns the underlying (shared) @fqref{TraitsData} "TraitsData" + instance held by this specification. + """ + return self.__data + + @classmethod + def create(cls): + """ + Returns a new instance of the Specification, holding a new + @fqref{TraitsData} "TraitsData" instance, pre-populated with all + of the specifications traits. + """ + data = TraitsData(cls.kTraitSet) + return cls(data) + + + def deprectatedTrait(self): + """ + Returns the view for the 'openassetio-example:example.Unchanged' trait wrapped around + the data held in this instance. + """ + return traits.example.DeprecatedTrait_v1(self.traitsData()) + + def updatedTrait(self): + """ + Returns the view for the 'openassetio-example:example.Updated' trait wrapped around + the data held in this instance. + """ + return traits.example.UpdatedTrait_v1(self.traitsData()) diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v1/traits/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/traits/__init__.py similarity index 100% rename from examples/resources/working_with_trait_versions/openassetio_example/v1/traits/__init__.py rename to examples/resources/working_with_trait_versions/openassetio_example/traits/__init__.py diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v1/traits/example.py b/examples/resources/working_with_trait_versions/openassetio_example/traits/example.py similarity index 60% rename from examples/resources/working_with_trait_versions/openassetio_example/v1/traits/example.py rename to examples/resources/working_with_trait_versions/openassetio_example/traits/example.py index d0ae54e..b44fe59 100644 --- a/examples/resources/working_with_trait_versions/openassetio_example/v1/traits/example.py +++ b/examples/resources/working_with_trait_versions/openassetio_example/traits/example.py @@ -12,12 +12,14 @@ from openassetio.trait import TraitsData -class RemovedTrait: +class DeprecatedTrait_v1: """ An example. Usage: entity, locale, relationship """ - kId = "openassetio-example:example.Removed.v1" + kId = "openassetio-example:example.Deprecated.v001" + + __deprecated__ = True # (Eventually use PEP 702) def __init__(self, traitsData): """ @@ -65,14 +67,16 @@ def imbueTo(cls, traitsData): traitsData.addTrait(cls.kId) +# Alias for latest version. +DeprecatedTrait = DeprecatedTrait_v1 -class UnchangedTrait: +class UpdatedTrait_v2: """ An example. Usage: entity, locale, relationship """ - kId = "openassetio-example:example.Unchanged.v1" + kId = "openassetio-example:example.Updated.v002" def __init__(self, traitsData): """ @@ -120,14 +124,95 @@ def imbueTo(cls, traitsData): traitsData.addTrait(cls.kId) + def setPropertyThatWasAdded(self, propertyThatWasAdded: float): + """ + Sets the propertyThatWasAdded property. + + A new property added in the latest version. + """ + if not isinstance(propertyThatWasAdded, float): + raise TypeError("propertyThatWasAdded must be a 'float'.") + self.__data.setTraitProperty(self.kId, "propertyThatWasAdded", propertyThatWasAdded) + + def getPropertyThatWasAdded(self, defaultValue: float=None) -> Union[float, None]: + """ + Gets the value of the propertyThatWasAdded property or the supplied default. + + A new property added in the latest version. + """ + value = self.__data.getTraitProperty(self.kId, "propertyThatWasAdded") + if value is None: + return defaultValue + + if not isinstance(value, float): + if defaultValue is None: + raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'float'.") + return defaultValue + return value + + def setPropertyThatWasRenamed(self, propertyThatWasRenamed: bool): + """ + Sets the propertyThatWasRenamed property. + + A property that has been renamed. + """ + if not isinstance(propertyThatWasRenamed, bool): + raise TypeError("propertyThatWasRenamed must be a 'bool'.") + self.__data.setTraitProperty(self.kId, "propertyThatWasRenamed", propertyThatWasRenamed) + + def getPropertyThatWasRenamed(self, defaultValue: bool=None) -> Union[bool, None]: + """ + Gets the value of the propertyThatWasRenamed property or the supplied default. + + A property that has been renamed. + """ + value = self.__data.getTraitProperty(self.kId, "propertyThatWasRenamed") + if value is None: + return defaultValue + + if not isinstance(value, bool): + if defaultValue is None: + raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'bool'.") + return defaultValue + return value + + def setPropertyToKeep(self, propertyToKeep: str): + """ + Sets the propertyToKeep property. + + A property that is unchanged between versions. + """ + if not isinstance(propertyToKeep, str): + raise TypeError("propertyToKeep must be a 'str'.") + self.__data.setTraitProperty(self.kId, "propertyToKeep", propertyToKeep) + + def getPropertyToKeep(self, defaultValue: str=None) -> Union[str, None]: + """ + Gets the value of the propertyToKeep property or the supplied default. + + A property that is unchanged between versions. + """ + value = self.__data.getTraitProperty(self.kId, "propertyToKeep") + if value is None: + return defaultValue + + if not isinstance(value, str): + if defaultValue is None: + raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'str'.") + return defaultValue + return value + + +# Alias to latest version +UpdatedTrait = UpdatedTrait_v2 -class UpdatedTrait: +class UpdatedTrait_v1: """ An example. Usage: entity, locale, relationship """ - kId = "openassetio-example:example.Updated.v1" + kId = "openassetio-example:example.Updated.v001" def __init__(self, traitsData): """ @@ -256,4 +341,58 @@ def getPropertyToRename(self, defaultValue: bool=None) -> Union[bool, None]: return value +class AddedTrait_v1: + """ + An example. + Usage: entity, locale, relationship + """ + kId = "openassetio-example:example.Added.v001" + + def __init__(self, traitsData): + """ + Construct this trait view, wrapping the given data. + + @param traitsData @fqref{TraitsData}} "TraitsData" The target + data that holds/will hold the traits properties. + """ + self.__data = traitsData + + def isImbued(self): + """ + Checks whether the data this trait has been applied to + actually has this trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return self.isImbuedTo(self.__data) + + @classmethod + def isImbuedTo(cls, traitsData): + """ + Checks whether the given data actually has this trait. + @param traitsData: Data to check for trait. + @return `True` if the underlying data has this trait, `False` + otherwise. + """ + return traitsData.hasTrait(cls.kId) + + def imbue(self): + """ + Adds this trait to the held data. + + If the data already has this trait, it is a no-op. + """ + self.__data.addTrait(self.kId) + + @classmethod + def imbueTo(cls, traitsData): + """ + Adds this trait to the provided data. + + If the data already has this trait, it is a no-op. + """ + traitsData.addTrait(cls.kId) + +# Alias to latest version +AddedTrait = AddedTrait_v1 \ No newline at end of file diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v1/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/v1/__init__.py deleted file mode 100644 index a755489..0000000 --- a/examples/resources/working_with_trait_versions/openassetio_example/v1/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -An example traits schema -""" - -# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. - -from . import traits -from . import specifications diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/example.py b/examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/example.py deleted file mode 100644 index 17c4409..0000000 --- a/examples/resources/working_with_trait_versions/openassetio_example/v1/specifications/example.py +++ /dev/null @@ -1,77 +0,0 @@ - -""" -Specification definitions in the 'example' namespace. - -Test specifications. -""" - -# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. - -from openassetio.trait import TraitsData - - -from .. import traits - - - -class ExampleSpecification: - """ - An example. - Usage: entity - """ - kTraitSet = { - # 'openassetio-example:example.Unchanged' - traits.example.UnchangedTrait.kId, - # 'openassetio-example:example.Updated' - traits.example.UpdatedTrait.kId, - - } - - def __init__(self, traitsData): - """ - Constructs the specification as a view on the supplied - shared @fqref{TraitsData} "TraitsData" instance. - - @param traitsData @fqref{TraitsData} "TraitsData" - - @warning Specifications are always a view on the supplied data, - which is held by reference. Any changes made to the data will be - visible to any other specifications or @ref trait "traits" that - wrap the same TraitsData instance. - """ - if not isinstance(traitsData, TraitsData): - raise TypeError("Specifications must be constructed with a TraitsData instance") - self.__data = traitsData - - def traitsData(self): - """ - Returns the underlying (shared) @fqref{TraitsData} "TraitsData" - instance held by this specification. - """ - return self.__data - - @classmethod - def create(cls): - """ - Returns a new instance of the Specification, holding a new - @fqref{TraitsData} "TraitsData" instance, pre-populated with all - of the specifications traits. - """ - data = TraitsData(cls.kTraitSet) - return cls(data) - - - def unchangedTrait(self): - """ - Returns the view for the 'openassetio-example:example.Unchanged' trait wrapped around - the data held in this instance. - """ - return traits.example.UnchangedTrait(self.traitsData()) - - def updatedTrait(self): - """ - Returns the view for the 'openassetio-example:example.Updated' trait wrapped around - the data held in this instance. - """ - return traits.example.UpdatedTrait(self.traitsData()) - \ No newline at end of file diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v2/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/v2/__init__.py deleted file mode 100644 index a755489..0000000 --- a/examples/resources/working_with_trait_versions/openassetio_example/v2/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -An example traits schema -""" - -# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. - -from . import traits -from . import specifications diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/__init__.py deleted file mode 100644 index 06aaee0..0000000 --- a/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Specifications defined in the 'openassetio-example' package. -""" - -# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. - -from . import example diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/example.py b/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/example.py deleted file mode 100644 index 4fee4bf..0000000 --- a/examples/resources/working_with_trait_versions/openassetio_example/v2/specifications/example.py +++ /dev/null @@ -1,77 +0,0 @@ - -""" -Specification definitions in the 'example' namespace. - -Test specifications. -""" - -# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. - -from openassetio.trait import TraitsData - - -from .. import traits - - - -class ExampleSpecification: - """ - An example specification. - Usage: entity - """ - kTraitSet = { - # 'openassetio-example:example.Unchanged' - traits.example.UnchangedTrait.kId, - # 'openassetio-example:example.Updated' - traits.example.UpdatedTrait.kId, - - } - - def __init__(self, traitsData): - """ - Constructs the specification as a view on the supplied - shared @fqref{TraitsData} "TraitsData" instance. - - @param traitsData @fqref{TraitsData} "TraitsData" - - @warning Specifications are always a view on the supplied data, - which is held by reference. Any changes made to the data will be - visible to any other specifications or @ref trait "traits" that - wrap the same TraitsData instance. - """ - if not isinstance(traitsData, TraitsData): - raise TypeError("Specifications must be constructed with a TraitsData instance") - self.__data = traitsData - - def traitsData(self): - """ - Returns the underlying (shared) @fqref{TraitsData} "TraitsData" - instance held by this specification. - """ - return self.__data - - @classmethod - def create(cls): - """ - Returns a new instance of the Specification, holding a new - @fqref{TraitsData} "TraitsData" instance, pre-populated with all - of the specifications traits. - """ - data = TraitsData(cls.kTraitSet) - return cls(data) - - - def unchangedTrait(self): - """ - Returns the view for the 'openassetio-example:example.Unchanged' trait wrapped around - the data held in this instance. - """ - return traits.example.UnchangedTrait(self.traitsData()) - - def updatedTrait(self): - """ - Returns the view for the 'openassetio-example:example.Updated' trait wrapped around - the data held in this instance. - """ - return traits.example.UpdatedTrait(self.traitsData()) - \ No newline at end of file diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/__init__.py b/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/__init__.py deleted file mode 100644 index 190df00..0000000 --- a/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Traits defined in the 'openassetio-example' package. -""" - -# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. - -from . import example diff --git a/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/example.py b/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/example.py deleted file mode 100644 index 9463e35..0000000 --- a/examples/resources/working_with_trait_versions/openassetio_example/v2/traits/example.py +++ /dev/null @@ -1,257 +0,0 @@ - -""" -Trait definitions in the 'example' namespace. - -Example namespace -""" - -# WARNING: This file is auto-generated by openassetio-traitgen, do not edit. - -from typing import Union - -from openassetio.trait import TraitsData - - -class AddedTrait: - """ - An example. - Usage: entity, locale, relationship - """ - kId = "openassetio-example:example.Added.v1" - - def __init__(self, traitsData): - """ - Construct this trait view, wrapping the given data. - - @param traitsData @fqref{TraitsData}} "TraitsData" The target - data that holds/will hold the traits properties. - """ - self.__data = traitsData - - def isImbued(self): - """ - Checks whether the data this trait has been applied to - actually has this trait. - @return `True` if the underlying data has this trait, `False` - otherwise. - """ - return self.isImbuedTo(self.__data) - - @classmethod - def isImbuedTo(cls, traitsData): - """ - Checks whether the given data actually has this trait. - @param traitsData: Data to check for trait. - @return `True` if the underlying data has this trait, `False` - otherwise. - """ - return traitsData.hasTrait(cls.kId) - - def imbue(self): - """ - Adds this trait to the held data. - - If the data already has this trait, it is a no-op. - """ - self.__data.addTrait(self.kId) - - @classmethod - def imbueTo(cls, traitsData): - """ - Adds this trait to the provided data. - - If the data already has this trait, it is a no-op. - """ - traitsData.addTrait(cls.kId) - - - - -class UnchangedTrait: - """ - An example. - Usage: entity, locale, relationship - """ - kId = "openassetio-example:example.Unchanged.v1" - - def __init__(self, traitsData): - """ - Construct this trait view, wrapping the given data. - - @param traitsData @fqref{TraitsData}} "TraitsData" The target - data that holds/will hold the traits properties. - """ - self.__data = traitsData - - def isImbued(self): - """ - Checks whether the data this trait has been applied to - actually has this trait. - @return `True` if the underlying data has this trait, `False` - otherwise. - """ - return self.isImbuedTo(self.__data) - - @classmethod - def isImbuedTo(cls, traitsData): - """ - Checks whether the given data actually has this trait. - @param traitsData: Data to check for trait. - @return `True` if the underlying data has this trait, `False` - otherwise. - """ - return traitsData.hasTrait(cls.kId) - - def imbue(self): - """ - Adds this trait to the held data. - - If the data already has this trait, it is a no-op. - """ - self.__data.addTrait(self.kId) - - @classmethod - def imbueTo(cls, traitsData): - """ - Adds this trait to the provided data. - - If the data already has this trait, it is a no-op. - """ - traitsData.addTrait(cls.kId) - - - - -class UpdatedTrait: - """ - An example. - Usage: entity, locale, relationship - """ - kId = "openassetio-example:example.Updated.v2" - - def __init__(self, traitsData): - """ - Construct this trait view, wrapping the given data. - - @param traitsData @fqref{TraitsData}} "TraitsData" The target - data that holds/will hold the traits properties. - """ - self.__data = traitsData - - def isImbued(self): - """ - Checks whether the data this trait has been applied to - actually has this trait. - @return `True` if the underlying data has this trait, `False` - otherwise. - """ - return self.isImbuedTo(self.__data) - - @classmethod - def isImbuedTo(cls, traitsData): - """ - Checks whether the given data actually has this trait. - @param traitsData: Data to check for trait. - @return `True` if the underlying data has this trait, `False` - otherwise. - """ - return traitsData.hasTrait(cls.kId) - - def imbue(self): - """ - Adds this trait to the held data. - - If the data already has this trait, it is a no-op. - """ - self.__data.addTrait(self.kId) - - @classmethod - def imbueTo(cls, traitsData): - """ - Adds this trait to the provided data. - - If the data already has this trait, it is a no-op. - """ - traitsData.addTrait(cls.kId) - - - def setPropertyThatWasAdded(self, propertyThatWasAdded: float): - """ - Sets the propertyThatWasAdded property. - - A new property added in the latest version. - """ - if not isinstance(propertyThatWasAdded, float): - raise TypeError("propertyThatWasAdded must be a 'float'.") - self.__data.setTraitProperty(self.kId, "propertyThatWasAdded", propertyThatWasAdded) - - def getPropertyThatWasAdded(self, defaultValue: float=None) -> Union[float, None]: - """ - Gets the value of the propertyThatWasAdded property or the supplied default. - - A new property added in the latest version. - """ - value = self.__data.getTraitProperty(self.kId, "propertyThatWasAdded") - if value is None: - return defaultValue - - if not isinstance(value, float): - if defaultValue is None: - raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'float'.") - return defaultValue - return value - - def setPropertyThatWasRenamed(self, propertyThatWasRenamed: bool): - """ - Sets the propertyThatWasRenamed property. - - A property that has been renamed. - """ - if not isinstance(propertyThatWasRenamed, bool): - raise TypeError("propertyThatWasRenamed must be a 'bool'.") - self.__data.setTraitProperty(self.kId, "propertyThatWasRenamed", propertyThatWasRenamed) - - def getPropertyThatWasRenamed(self, defaultValue: bool=None) -> Union[bool, None]: - """ - Gets the value of the propertyThatWasRenamed property or the supplied default. - - A property that has been renamed. - """ - value = self.__data.getTraitProperty(self.kId, "propertyThatWasRenamed") - if value is None: - return defaultValue - - if not isinstance(value, bool): - if defaultValue is None: - raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'bool'.") - return defaultValue - return value - - def setPropertyToKeep(self, propertyToKeep: str): - """ - Sets the propertyToKeep property. - - A property that is unchanged between versions. - """ - if not isinstance(propertyToKeep, str): - raise TypeError("propertyToKeep must be a 'str'.") - self.__data.setTraitProperty(self.kId, "propertyToKeep", propertyToKeep) - - def getPropertyToKeep(self, defaultValue: str=None) -> Union[str, None]: - """ - Gets the value of the propertyToKeep property or the supplied default. - - A property that is unchanged between versions. - """ - value = self.__data.getTraitProperty(self.kId, "propertyToKeep") - if value is None: - return defaultValue - - if not isinstance(value, str): - if defaultValue is None: - raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'str'.") - return defaultValue - return value - - - diff --git a/examples/resources/working_with_trait_versions/v1.yml b/examples/resources/working_with_trait_versions/v1.yml deleted file mode 100644 index 51b38cd..0000000 --- a/examples/resources/working_with_trait_versions/v1.yml +++ /dev/null @@ -1,62 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/OpenAssetIO/OpenAssetIO-TraitGen/main/python/openassetio_traitgen/schema.json -# yamllint disable-line rule:document-start -package: openassetio-example -version: 1 -description: An example traits schema - -traits: - example: - description: Example namespace - members: - Unchanged: - version: 1 - description: An example. - usage: - - entity - - locale - - relationship - - Removed: - version: 1 - description: An example. - usage: - - entity - - locale - - relationship - - Updated: - version: 1 - description: An example. - usage: - - entity - - locale - - relationship - properties: - propertyToKeep: - type: string - description: A property that is unchanged between versions. - propertyToRename: - type: boolean - description: > - A property that has an inappropriate name and should be renamed in the - next version. - propertyToRemove: - type: boolean - description: A defunct property that should be removed in the next version. - -specifications: - example: - description: Test specifications. - members: - - Example: - description: An example. - usage: - - entity - traitSet: - - namespace: example - name: Unchanged - version: 1 - - namespace: example - name: Updated - version: 1 diff --git a/examples/resources/working_with_trait_versions/v2.yml b/examples/resources/working_with_trait_versions/v2.yml deleted file mode 100644 index c1f7b1b..0000000 --- a/examples/resources/working_with_trait_versions/v2.yml +++ /dev/null @@ -1,61 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/OpenAssetIO/OpenAssetIO-TraitGen/main/python/openassetio_traitgen/schema.json -# yamllint disable-line rule:document-start -package: openassetio-example -version: 2 -description: An example traits schema - -traits: - example: - description: Example namespace - members: - Unchanged: - version: 1 - description: An example. - usage: - - entity - - locale - - relationship - - Added: - version: 1 - description: An example. - usage: - - entity - - locale - - relationship - - Updated: - version: 2 - description: An example. - usage: - - entity - - locale - - relationship - properties: - propertyToKeep: - type: string - description: A property that is unchanged between versions. - propertyThatWasRenamed: - type: boolean - description: A property that has been renamed. - propertyThatWasAdded: - type: float - description: A new property added in the latest version. - -specifications: - example: - description: Test specifications. - members: - - Example: - description: An example specification. - usage: - - entity - traitSet: - - namespace: example - name: Unchanged - version: 1 - - namespace: example - name: Updated - version: 2 - diff --git a/examples/working_with_trait_versions.ipynb b/examples/working_with_trait_versions.ipynb index 8af4fc7..cad22cf 100644 --- a/examples/working_with_trait_versions.ipynb +++ b/examples/working_with_trait_versions.ipynb @@ -25,11 +25,8 @@ { "cell_type": "markdown", "source": [ - "### Schema subpackages\n", "\n", - "Trait packages generated by `traitgen` can include subpackages for all available schema versions, with the top-level namespace containing aliases to the most-recent available version.\n", - "\n", - "To illustrate this, we use the versioned trait mockups under `resources/working_with_trait_versions`. \n" + "For illustration, we use the versioned trait mockups under `resources/working_with_trait_versions`. \n" ], "metadata": { "collapsed": false @@ -38,64 +35,28 @@ }, { "cell_type": "code", - "outputs": [], - "source": [ - "from resources.working_with_trait_versions.openassetio_example import traits, specifications\n", - "from resources.working_with_trait_versions.openassetio_example import v1\n", - "from resources.working_with_trait_versions.openassetio_example import v2\n", - "\n", - "\n", - "assert traits is v2.traits\n", - "assert specifications is v2.specifications\n", - "\n", - "assert v1.traits is not v2.traits\n", - "assert v1.traits is not v2.specifications" - ], + "source": "from resources.working_with_trait_versions.openassetio_example import traits, specifications", "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-04-17T13:35:09.501325Z", - "start_time": "2024-04-17T13:35:09.489263Z" + "end_time": "2024-05-07T12:30:33.824668Z", + "start_time": "2024-05-07T12:30:33.814780Z" } }, "id": "ed3207872eaa89b0", + "outputs": [], "execution_count": 1 }, { "cell_type": "markdown", "source": [ - "Host applications that bundle the trait package may use the non-versioned top-level package by default. \n", + "### Versioned trait views\n", "\n", - "For Python manager plugins, if \n", + "We imagine an industry where there are only 3 traits, `AddedTrait`, `DeprecatedTrait`, and `UpdatedTrait`, which are used across entities, relationships, policies and locales. The traits themselves have no meaning, they are named purely to give a hint as to how they are versioned.\n", "\n", - "* the host application's largest (most-recent) schema version does not match the schema version the Python plugin was developed against; and\n", - "* the Python plugin uses the default (top-level) traits/specifications packages\n", - "\n", - "the application may hit unexpected runtime errors. This is because the structure of trait/specification view classes may be different, or simply not exist.\n", + "Within the package, trait views for all previous versions are included, with a version suffix on the class name. The class name without a version suffix is an alias to the latest version available in the package.\n", "\n", - "A Python manager plugin should therefore use a versioned namespace. This solves the problem when the plugin is loaded into an application that defaults to a _higher_ schema version than the plugin was developed for. \n", - "\n", - "However, if the host application's largest available schema version is too _low_ for the manager plugin, then using a versioned namespace would suffer an `ImportError` when attempting to `import` the (too new) versioned namespace. This can either be left to fail (better to fail early), or be tolerated within the plugin by catching the exception and falling back to an older version.\n", - "\n", - "C++ plugins have the trait view classes (privately) compiled into them, so do not depend on the schema version that the host application was built against. Incompatibilities only become apparent at runtime, when incoming trait data is found to be of a version unsupported by the manager or host." - ], - "metadata": { - "collapsed": false - }, - "id": "e96f0b057e4fd49c" - }, - { - "cell_type": "markdown", - "source": [ - "### Trait views within subpackages\n", - "\n", - "We imagine an industry where there are only 4 traits, `AddedTrait`, `RemovedTrait`, `UnchangedTrait` and `UpdatedTrait`, which are used across entities, relationships, policies and locales. The traits themselves have no meaning, they are named purely to give a hint as to how they change in subsequent trait schema versions.\n", - "\n", - "Within a subpackage, all traits within that subpackage's schema version are included, with unchanged traits being duplicated. Duplication may be replaced with aliasing in a future update of `traitgen`.\n", - "\n", - "The current and any older versions of a trait are also represented under a subpackage, with a version suffix on the class name. The class name without a version suffix is an alias to the latest version available under that schema version.\n", - "\n", - "Traits can be removed from a schema, but will still exist under the schema subpackage if the package was generated to include older schema versions." + "Traits can be marked as deprecated to signal that they will eventually be removed from the schema altogether." ], "metadata": { "collapsed": false @@ -104,72 +65,83 @@ }, { "cell_type": "code", - "outputs": [], "source": [ - "assert v2.traits.example.UnchangedTrait is not v1.traits.example.UnchangedTrait\n", - "assert v2.traits.example.UpdatedTrait is not v1.traits.example.UpdatedTrait\n", - "\n", - "assert v2.traits.example.UpdatedTrait is v2.traits.example.UpdatedTrait_v2\n", - "assert v2.traits.example.UpdatedTrait is not v2.traits.example.UpdatedTrait_v1\n", - "assert \"UpdatedTrait_v2\" not in v1.traits.example.__dict__\n", + "assert traits.example.UpdatedTrait is traits.example.UpdatedTrait_v2\n", + "assert traits.example.UpdatedTrait is not traits.example.UpdatedTrait_v1\n", "\n", - "# RemovedTrait is missing from v2, but is still added to the v2\n", - "# subpackage to aid backward compatibility.\n", - "assert \"RemovedTrait\" in v2.traits.example.__dict__\n", - "assert \"RemovedTrait\" in v1.traits.example.__dict__\n", + "assert traits.example.AddedTrait is traits.example.AddedTrait_v1\n", "\n", - "assert \"AddedTrait\" in v2.traits.example.__dict__\n", - "assert \"AddedTrait\" not in v1.traits.example.__dict__" + "assert traits.example.DeprecatedTrait is traits.example.DeprecatedTrait_v1\n", + "assert traits.example.DeprecatedTrait_v1.__deprecated__" ], "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-04-17T13:54:03.525882Z", - "start_time": "2024-04-17T13:54:03.513393Z" + "end_time": "2024-05-07T12:30:33.841243Z", + "start_time": "2024-05-07T12:30:33.825997Z" } }, "id": "1470d16817a82421", - "execution_count": 10 + "outputs": [], + "execution_count": 2 }, { "cell_type": "markdown", "source": [ - "Each trait encodes its version in its unique ID. \n", + "Host applications that bundle the trait package may use the non-version-suffixed classes by default.\n", + " \n", + "For Python manager plugins, if\n", + "\n", + "* the host application's largest (most-recent) set of versions does not match the versions the Python plugin was developed against; and\n", + "* the Python plugin uses the default (non-suffixed) traits/specifications classes\n", + " \n", + "the application may hit unexpected runtime errors. This is because the structure of trait/specification view classes may be different, or simply not exist.\n", "\n", - "The schema version is bumped whenever one or more traits or specifications are added/removed/updated, forming the next schema version. This means the maximum possible version of a trait is bounded by the top-level schema version (inclusive). Adding a new trait will result in a schema version bump, but the new trait itself will start at version 1." + "A Python manager plugin should therefore use version-suffixed classses. This solves the problem when the plugin is loaded into an application that defaults to a _higher_ set of versions than the plugin was developed for.\n", + " \n", + "However, if the host application's largest available versions are too _low_ for the manager plugin, then using a version-suffixed class would suffer an `ImportError` when attempting to `import` the (too new) versioned class. This can either be left to fail (better to fail early), or be tolerated within the plugin by catching the exception and falling back to an older version\n", + " \n", + "C++ plugins have the trait view classes (privately) compiled into them, so do not depend on the versions that the host application was built against/ships with. Incompatibilities only become apparent at runtime, when incoming trait data is found to be of a version unsupported by the manager or host." ], "metadata": { "collapsed": false }, "id": "75a1877c0c9c3786" }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Trait IDs\n", + "\n", + "Each trait encodes its version in its unique ID, up to a maximum version of 999, and zero-padded, such that `kId[:-5]` can be used to strip the version tag." + ], + "id": "59b7afb066b5bcc9" + }, { "cell_type": "code", - "outputs": [], "source": [ - "# An updated trait has a different ID in each subpackage.\n", - "assert v1.traits.example.UpdatedTrait.kId == \"openassetio-example:example.Updated.v1\"\n", - "assert v2.traits.example.UpdatedTrait.kId == \"openassetio-example:example.Updated.v2\"\n", + "# An updated trait has a different ID for each version.\n", + "assert traits.example.UpdatedTrait_v1.kId == \"openassetio-example:example.Updated.v001\"\n", + "assert traits.example.UpdatedTrait_v2.kId == \"openassetio-example:example.Updated.v002\"\n", "\n", - "# A newly added trait starts at version 1, despite the schema\n", - "# (subpackage) version being greater than 1.\n", - "assert v2.traits.example.AddedTrait.kId == \"openassetio-example:example.Added.v1\"" + "# A newly added trait starts at version 1.\n", + "assert traits.example.AddedTrait.kId == \"openassetio-example:example.Added.v001\"" ], "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-04-17T13:35:09.531776Z", - "start_time": "2024-04-17T13:35:09.516191Z" + "end_time": "2024-05-07T12:30:33.857952Z", + "start_time": "2024-05-07T12:30:33.842191Z" } }, "id": "a31f8a7263370714", + "outputs": [], "execution_count": 3 }, { "cell_type": "markdown", - "source": [ - "This means trait view classes from one schema version cannot be used to read traits of another version, unless the trait is unchanged between schema versions." - ], + "source": "This means trait view classes from one version cannot be used to read another version.", "metadata": { "collapsed": false }, @@ -177,81 +149,54 @@ }, { "cell_type": "code", - "outputs": [], "source": [ "from openassetio.trait import TraitsData\n", "\n", "\n", - "v1_data, v2_data, unchanged_data = TraitsData(), TraitsData(), TraitsData()\n", - "\n", - "v1.traits.example.UpdatedTrait.imbueTo(v1_data)\n", - "v2.traits.example.UpdatedTrait.imbueTo(v2_data)\n", - "v1.traits.example.UnchangedTrait.imbueTo(unchanged_data)\n", + "v1_data = TraitsData()\n", "\n", - "assert v1.traits.example.UpdatedTrait.isImbuedTo(v1_data) is True\n", - "assert v1.traits.example.UpdatedTrait.isImbuedTo(v2_data) is False\n", + "traits.example.UpdatedTrait_v1.imbueTo(v1_data)\n", "\n", - "assert v2.traits.example.UpdatedTrait.isImbuedTo(v1_data) is False\n", - "assert v2.traits.example.UpdatedTrait.isImbuedTo(v2_data) is True\n", - "\n", - "assert v1.traits.example.UnchangedTrait.isImbuedTo(unchanged_data) is True\n", - "assert v2.traits.example.UnchangedTrait.isImbuedTo(unchanged_data) is True" + "assert traits.example.UpdatedTrait.isImbuedTo(v1_data) is False\n", + "assert traits.example.UpdatedTrait_v1.isImbuedTo(v1_data) is True\n", + "assert traits.example.UpdatedTrait_v2.isImbuedTo(v1_data) is False\n" ], "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-04-17T13:35:09.544703Z", - "start_time": "2024-04-17T13:35:09.533132Z" + "end_time": "2024-05-07T12:30:33.870856Z", + "start_time": "2024-05-07T12:30:33.858926Z" } }, "id": "a147006dd4b9b453", + "outputs": [], "execution_count": 4 }, { "metadata": {}, - "cell_type": "markdown", - "source": "However, a workaround to this is to use the fallback versioned trait view classes in the newer schema subpackage", - "id": "c5d303a5d19954b4" - }, - { - "metadata": {}, - "cell_type": "code", - "outputs": [], - "execution_count": null, - "source": [ - "assert v2.traits.example.UpdatedTrait_v1.isImbuedTo(v1_data) is True\n", - "assert v2.traits.example.UpdatedTrait_v2.isImbuedTo(v2_data) is True" - ], - "id": "74707caa0ec8f1f6" - }, - { "cell_type": "markdown", "source": [ "### Specifications\n", "\n", "Specifications are a way to document well-known sets of traits that categorize entities, relationships, locales, or policies. Agreement on these as an industry is crucial for effective interop.\n", "\n", - "However, they do not have an independent version - their version is implicit in the (versioned) traits that they compose, and in the overall schema version where they are defined. A major consequence of this is that no specification version is embedded in the data itself.\n", + "However, no specification version is embedded in the data itself. The data sent between host and manager only contains the versions of the composite traits. Detecting that some trait data satisfies a specification therefore requires a try-and-see approach, checking the data against the specification's trait set.\n", "\n", "If the raison d'être of specifications is a way to have an industry-standard collection of trait sets, then is the exact version of those traits really important? On the assumption that a new version of a trait doesn't fundamentally change its meaning (otherwise it would be an entirely new trait), then it's reasonable to say that specifications are trait version agnostic.\n", "\n", "So specifications are invaluable as documentation of common trait sets. However, the auto-generateed `Specification` view classes should be used with caution: using a specification view class to categorize an entity may lead to unexpected false negatives when the trait versions do not line up.\n" ], - "metadata": { - "collapsed": false - }, "id": "ab4dde470a3808d4" }, { "cell_type": "code", - "outputs": [], "source": [ "entity_data = TraitsData(\n", - " {v1.traits.example.UnchangedTrait.kId, v1.traits.example.UpdatedTrait.kId})\n", + " {traits.example.AddedTrait.kId, traits.example.UpdatedTrait_v1.kId})\n", "\n", "# As long as the trait set of the specification lines up with the\n", "# incoming data, we can use it to categorize an entity.\n", - "is_an_example_entity = v1.specifications.example.ExampleSpecification.kTraitSet.issubset(\n", + "is_an_example_entity = specifications.example.ExampleSpecification_v1.kTraitSet.issubset(\n", " entity_data.traitSet())\n", "assert is_an_example_entity is True\n", "\n", @@ -259,18 +204,19 @@ "# to the specification mean the version suffix on some trait IDs have\n", "# updated and no longer match the incoming data, so we get a false\n", "# negative.\n", - "is_an_example_entity = v2.specifications.example.ExampleSpecification.kTraitSet.issubset(\n", - " entity_data.traitSet())\n", + "is_an_example_entity = specifications.example.ExampleSpecification.kTraitSet.issubset(\n", + " entity_data.traitSet()) # or ExampleSpecification_v2\n", "assert is_an_example_entity is False\n" ], "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-04-17T13:35:09.557999Z", - "start_time": "2024-04-17T13:35:09.545693Z" + "end_time": "2024-05-07T12:30:33.887271Z", + "start_time": "2024-05-07T12:30:33.872435Z" } }, "id": "a94f4bcd14862e3a", + "outputs": [], "execution_count": 5 }, { @@ -291,7 +237,6 @@ }, { "cell_type": "code", - "outputs": [], "source": [ "from openassetio.hostApi import HostInterface\n", "from openassetio.managerApi import Host, HostSession\n", @@ -311,11 +256,12 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-04-17T13:35:09.575258Z", - "start_time": "2024-04-17T13:35:09.558968Z" + "end_time": "2024-05-07T12:30:33.904152Z", + "start_time": "2024-05-07T12:30:33.890148Z" } }, "id": "592a08fb134655a", + "outputs": [], "execution_count": 6 }, { @@ -334,12 +280,12 @@ }, { "cell_type": "code", - "outputs": [], "source": [ "from openassetio.managerApi import ManagerInterface, EntityReferencePagerInterface\n", "from openassetio import errors, access, EntityReference\n", "\n", - "from resources.working_with_trait_versions.openassetio_example.v2 import traits\n", + "from resources.working_with_trait_versions.openassetio_example import traits\n", + "\n", "\n", "an_entity_ref_str = \"example://entity\"\n", "\n", @@ -377,7 +323,7 @@ "\n", " # Assume we know from reading release notes that UnchangedTrait\n", " # hasn't changed, so we can use v2 knowing that v1 is equivalent.\n", - " if traits.example.UnchangedTrait.isImbuedTo(context.locale):\n", + " if traits.example.AddedTrait.isImbuedTo(context.locale):\n", " # UpdatedTrait changes between versions, but we know from\n", " # release notes that the property we're interested in still\n", " # exists semantically, it's just the name has changed.\n", @@ -386,7 +332,7 @@ " # the property via its new name.\n", " if traits.example.UpdatedTrait_v2.isImbuedTo(context.locale):\n", " is_locale_special = True\n", - " special_locale_value = v2.traits.example.UpdatedTrait(\n", + " special_locale_value = traits.example.UpdatedTrait_v2(\n", " context.locale).getPropertyThatWasRenamed(defaultValue=special_locale_value)\n", "\n", " # If v2 of UpdatedTrait was not imbued, fall back to v1. If\n", @@ -404,10 +350,11 @@ " if not is_locale_special:\n", " continue\n", "\n", - " # Only sets with the v2 Added or v1 Removed trait are\n", - " # supported.\n", - " if not (traits.example.RemovedTrait.kId in trait_set or\n", - " traits.example.AddedTrait.kId in trait_set):\n", + " # Only Example entities are supported, though we support both v1 and v2.\n", + " if (not specifications.example.ExampleSpecification_v2.kTraitSet.issubset(\n", + " trait_set)\n", + " and not specifications.example.ExampleSpecification_v1.kTraitSet.issubset(\n", + " trait_set)):\n", " continue\n", "\n", " if special_locale_value is True:\n", @@ -450,20 +397,16 @@ " self, traitSets, defaultEntityAccess, context, hostSession, successCallback,\n", " errorCallback):\n", " for idx, trait_set in enumerate(traitSets):\n", - " is_an_unchanged = traits.example.UnchangedTrait.kId in trait_set\n", " is_an_updated = (traits.example.UpdatedTrait_v2.kId in trait_set or\n", " traits.example.UpdatedTrait_v1.kId in trait_set)\n", - " is_a_removed = traits.example.RemovedTrait.kId in trait_set\n", + " is_a_deprecated = traits.example.DeprecatedTrait.kId in trait_set\n", " is_an_added = traits.example.AddedTrait.kId in trait_set\n", "\n", - " if is_an_unchanged and is_an_updated and is_a_removed:\n", - " # Only possible with v1 schema/trait\n", - " entity_ref = \"example://default/removed\"\n", - " elif is_an_unchanged and is_an_updated and is_an_added:\n", - " # Only possible with v2 schema/trait\n", + " if is_an_updated and is_a_deprecated:\n", + " entity_ref = \"example://default/deprecated\"\n", + " elif is_an_updated and is_an_added:\n", " entity_ref = \"example://default/added\"\n", - " elif is_an_unchanged and is_an_updated:\n", - " # v1 or v2 schemas\n", + " elif is_an_updated:\n", " entity_ref = \"example://default\"\n", " else:\n", " # Any other unrecognized trait set\n", @@ -498,13 +441,13 @@ " # We use the ExampleSpecification - a well-known\n", " # trait set with all traits required to categorize\n", " # an entity as an Example. No way to know what\n", - " # version the host would prefer. So prefer latest,\n", - " # v2.\n", - " successCallback(idx, v2.specifications.example.ExampleSpecification.kTraitSet)\n", + " # version the host would prefer. So prefer default,\n", + " # which will be the latest, v2.\n", + " successCallback(idx, specifications.example.ExampleSpecification.kTraitSet)\n", " else:\n", " # Minimum required for publishing is a reduced set.\n", " successCallback(\n", - " idx, v2.specifications.example.ExampleSpecification.kTraitSet - {\n", + " idx, specifications.example.ExampleSpecification.kTraitSet - {\n", " traits.example.AddedTrait.kId})\n", "\n", " else:\n", @@ -569,26 +512,27 @@ "\n", " # Parse out important aspects of the type of relationship.\n", "\n", - " is_rel_unchanged = traits.example.UnchangedTrait.isimbuedTo(\n", + " is_rel_added = traits.example.AddedTrait.isimbuedTo(\n", + " relationshipTraitsData)\n", + " is_rel_deprecated = traits.example.DeprecatedTrait.isimbuedTo(\n", " relationshipTraitsData)\n", "\n", " rel_v2_updated = traits.example.UpdatedTrait_v2(relationshipTraitsData)\n", " rel_v1_updated = traits.example.UpdatedTrait_v1(relationshipTraitsData)\n", " is_rel_updated = rel_v2_updated.isImbued() or rel_v1_updated.isImbued()\n", "\n", + " # Some important property used to filter the list of related entities.\n", " important_property = rel_v2_updated.getPropertyThatWasRenamed(\n", " defaultValue=rel_v1_updated.getPropertyToRename(defaultValue=False))\n", "\n", - " is_a_child_relationship = is_rel_unchanged and is_rel_updated\n", + " is_a_child_relationship = (is_rel_added or is_rel_deprecated) and is_rel_updated\n", "\n", " # Parse out important aspects of the type of expected result entity.\n", "\n", " result_type = \"none\"\n", - " if {traits.example.AddedTrait.kId,\n", - " traits.example.UnchangedTrait.kId}.issubset(resultTraitSet):\n", + " if {traits.example.AddedTrait.kId}.issubset(resultTraitSet):\n", " result_type = \"component\"\n", - " elif {traits.example.RemovedTrait.kId,\n", - " traits.example.UnchangedTrait.kId}.issubset(resultTraitSet):\n", + " elif {traits.example.DeprecatedTrait.kId}.issubset(resultTraitSet):\n", " result_type = \"element\"\n", "\n", " # Loop through input entities.\n", @@ -669,16 +613,15 @@ " continue\n", "\n", " # Categorise the data to publish\n", - " is_an_unchanged = traits.example.UnchangedTrait.isImbuedTo(traits_data)\n", " is_an_updated = (traits.example.UpdatedTrait_v2.isImbuedTo(traits_data) or\n", " traits.example.UpdatedTrait_v1.isImbuedTo(traits_data))\n", - " is_a_removed = traits.example.RemovedTrait.isImbuedTo(traits_data)\n", + " is_a_deprecated = traits.example.DeprecatedTrait.isImbuedTo(traits_data)\n", " is_an_added = traits.example.AddedTrait.isImbuedTo(traits_data)\n", "\n", - " # Based on the given traits, categorize to pipeline-specific, \"type\"\n", + " # Based on the given traits, categorize to pipeline-specific \"type\"\n", " is_an_item = is_an_added and is_an_updated\n", - " is_a_unit = is_a_removed and is_an_updated\n", - " is_an_ingredient = is_an_unchanged and is_an_updated\n", + " is_a_unit = is_a_deprecated and is_an_updated\n", + " is_an_ingredient = not is_an_added and not is_a_deprecated and is_an_updated\n", "\n", " # At least and only one, i.e. n-ary xor.\n", " if int(is_an_item) + int(is_a_unit) + int(is_an_ingredient) != 1:\n", @@ -726,16 +669,15 @@ " continue\n", "\n", " # Categorise the data to publish\n", - " is_an_unchanged = traits.example.UnchangedTrait.isImbuedTo(traits_data)\n", " is_an_updated = (traits.example.UpdatedTrait_v2.isImbuedTo(traits_data) or\n", " traits.example.UpdatedTrait_v1.isImbuedTo(traits_data))\n", - " is_a_removed = traits.example.RemovedTrait.isImbuedTo(traits_data)\n", + " is_a_deprecated = traits.example.DeprecatedTrait.isImbuedTo(traits_data)\n", " is_an_added = traits.example.AddedTrait.isImbuedTo(traits_data)\n", "\n", " # Based on the given traits, categorize to pipeline-specific, \"type\"\n", " is_an_item = is_an_added and is_an_updated\n", - " is_a_unit = is_a_removed and is_an_updated\n", - " is_an_ingredient = is_an_unchanged and is_an_updated\n", + " is_a_unit = is_a_deprecated and is_an_updated\n", + " is_an_ingredient = not is_an_added and not is_a_deprecated and is_an_updated\n", "\n", " # At least and only one, i.e. n-ary xor.\n", " if int(is_an_item) + int(is_a_unit) + int(is_an_ingredient) != 1:\n", @@ -807,20 +749,13 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-04-17T15:20:50.073149Z", - "start_time": "2024-04-17T15:20:49.992403Z" + "end_time": "2024-05-07T12:30:33.946754Z", + "start_time": "2024-05-07T12:30:33.906750Z" } }, "id": "5bb5454ca1ae8b06", - "execution_count": 11 - }, - { - "metadata": {}, - "cell_type": "code", "outputs": [], - "execution_count": null, - "source": "", - "id": "64a4f3ecdfba5690" + "execution_count": 7 }, { "cell_type": "markdown", @@ -836,7 +771,6 @@ }, { "cell_type": "code", - "outputs": [], "source": [ "from openassetio.hostApi import Manager\n", "\n", @@ -848,20 +782,15 @@ "\n", "# Configure the locale\n", "\n", - "context.locale.addTraits(v2.specifications.example.ExampleSpecification.kTraitSet)\n", + "context.locale.addTraits(specifications.example.ExampleSpecification.kTraitSet)\n", "\n", "# The minimum set of traits required to publish to this entity\n", "# reference.\n", "minimum_trait_set = manager.entityTraits(an_entity_ref, access.EntityTraitsAccess.kWrite, context)\n", "\n", - "# Schema version to use for traits/specifications when creating new\n", - "# trait sets/data. Downgrade when manager seems to indicate it doesn't\n", - "# support the latest version.\n", - "preferred_schema_version = 2\n", - "\n", "# Whatever the minimum trait set is, we know we're going to publish an\n", "# Example.\n", - "desired_trait_set = minimum_trait_set | v2.specifications.example.ExampleSpecification.kTraitSet\n", + "desired_trait_set = minimum_trait_set | specifications.example.ExampleSpecification.kTraitSet\n", "\n", "# Get the set of traits that have properties the manager can persist.\n", "[policy_for_desired_traits] = manager.managementPolicy(\n", @@ -874,7 +803,7 @@ "# properties).\n", "if not traits.example.UpdatedTrait_v2.isImbuedTo(policy_for_desired_traits):\n", " # v2 didn't work, try v1.\n", - " desired_trait_set = minimum_trait_set | v1.specifications.example.ExampleSpecification.kTraitSet\n", + " desired_trait_set = minimum_trait_set | specifications.example.ExampleSpecification_v1.kTraitSet\n", "\n", " # Get the set of traits that have properties the manager can\n", " # persist.\n", @@ -885,22 +814,15 @@ " # v1 didn't work either, bail.\n", " raise Exception(f\"Cannot publish an Example to ref {an_entity_ref}\")\n", "\n", - " # We know the manager doesn't support v2, so downgrade the schema\n", - " # version to use when constructing new trait sets/data. Note that\n", - " # there's no guarantee that the schema version we picked will work\n", - " # for every trait set - for example, the manager may have mixed\n", - " # trait support across schema versions.\n", - " preferred_schema_version = 1\n", - "\n", "# Filter down the desired traits to only those that are supported.\n", "trait_set_to_publish = desired_trait_set & policy_for_desired_traits.traitSet()\n", "\n", "# We want to keep (the minimum amount of) data from the previous\n", "# version, except for the values we're going to provide.\n", - "if preferred_schema_version == 2:\n", - " trait_set_to_keep = trait_set_to_publish - v2.specifications.example.ExampleSpecification.kTraitSet\n", + "if specifications.example.ExampleSpecification_v2.kTraitSet.issubset(desired_trait_set):\n", + " trait_set_to_keep = trait_set_to_publish - specifications.example.ExampleSpecification_v2.kTraitSet\n", "else:\n", - " trait_set_to_keep = trait_set_to_publish - v1.specifications.example.ExampleSpecification.kTraitSet\n", + " trait_set_to_keep = trait_set_to_publish - specifications.example.ExampleSpecification_v1.kTraitSet\n", "\n", "# Get the properties that we wish to keep from the current version.\n", "data_to_publish = manager.resolve(\n", @@ -928,10 +850,16 @@ "else:\n", " # If the manager doesn't want to provide a value for entities of\n", " # this type, use a default.\n", - " if preferred_schema_version == 2:\n", - " traits.example.UpdatedTrait_v2(data_to_publish).setPropertyThatWasRenamed(True)\n", + " # Here we use the Specification view class, rather than the trait\n", + " # view class. We could equivalently use `UpdatedTrait` directly. By\n", + " # using the Specification view class we can be certain of the\n", + " # compatibility of the trait view class version.\n", + " if specifications.example.ExampleSpecification_v2.kTraitSet.issubset(desired_trait_set):\n", + " specifications.example.ExampleSpecification_v2(\n", + " data_to_publish).updatedTrait().setPropertyThatWasRenamed(True)\n", " else:\n", - " traits.example.UpdatedTrait_v1(data_to_publish).setPropertyToRename(True)\n", + " specifications.example.ExampleSpecification_v1(\n", + " data_to_publish).updatedTrait().setPropertyToRename(True)\n", "\n", "# We can now successfully begin the publishing process.\n", "working_ref = manager.preflight(\n", @@ -965,11 +893,12 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-04-17T13:35:09.638280Z", - "start_time": "2024-04-17T13:35:09.624587Z" + "end_time": "2024-05-07T12:30:33.963775Z", + "start_time": "2024-05-07T12:30:33.947772Z" } }, "id": "b8e12c55d88bda27", + "outputs": [], "execution_count": 8 }, { @@ -981,9 +910,9 @@ "\n", "There are a few cases where the version of a trait is unimportant, i.e. where the properties are not required since we only wish to use the traits to categorize an entity/relationship/policy/locale. Those cases may warrant API changes to reduce boilerplate. In particular, Specification view classes are currently unsuitable for this use-case. However, this boilerplate does not _prevent_ workflows. Specifications can be used as documentation to help authors construct their own trait set detection logic. So the addition of utility functions is not critical to working with versioned traits.\n", "\n", - "Conversely, when constructing a trait set or data, a schema version must be chosen by the host/manager. In the case of hosts adapting to managers, the `managementPolicy` API method provides a negotiation mechanism. Otherwise, the host/manager must simply choose a preferred schema version. It is in these circumstances, where new trait data is being created, that Specification view classes are particularly useful.\n", + "Conversely, when constructing a trait set or data, a trait/specification version must be chosen by the host/manager. In the case of hosts adapting to managers, the `managementPolicy` API method provides a negotiation mechanism. Otherwise, the host/manager must simply choose a preferred version. It is in these circumstances, where new trait data is being created, that Specification view classes are particularly useful.\n", "\n", - "It seems clear that dealing with mixed trait versions adds a lot of branching logic that could be hard to follow and maintain. A tempting solution is to expose the preferred schema version as a queryable value, so that branching can be performed at a higher level. However, as discussed in [DR023](https://github.com/OpenAssetIO/OpenAssetIO/blob/main/doc/decisions/DR023-Versioning-traits-and-specifications-method.md), this precludes many important workflows, since the ultimate provenance of trait data is unknown in the general case. For example, the manager may combine old data from a database with newly generated data; or the host may load an old project file holding trait data of a previous schema version; or multiple components of a system, each working with a different schema version, may collaborate to produce a trait set/data." + "It seems clear that dealing with mixed trait versions adds a lot of branching logic that could be hard to follow and maintain. However, as discussed in [DR023](https://github.com/OpenAssetIO/OpenAssetIO/blob/main/doc/decisions/DR023-Versioning-traits-and-specifications-method.md), branching at a higher level precludes many important workflows, so there seems no way around this." ], "metadata": { "collapsed": false From 928408a7ff0f5039b04f4741b4471040d6176314 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Thu, 16 May 2024 14:28:31 +0100 Subject: [PATCH 4/4] squash: Ensure versioning is a non-breaking change to legacy Version 1 should be a special case, where the ID is not suffixed with a version tag, and the unversioned class name continues to be available. This then means the introduction of versioning is a non-breaking change to people currently using the legacy library. Signed-off-by: David Feltell --- .../specifications/example.py | 19 +- .../openassetio_example/traits/example.py | 89 ++++++--- examples/working_with_trait_versions.ipynb | 184 +++++++++++------- 3 files changed, 186 insertions(+), 106 deletions(-) diff --git a/examples/resources/working_with_trait_versions/openassetio_example/specifications/example.py b/examples/resources/working_with_trait_versions/openassetio_example/specifications/example.py index 4c6a068..ef36ffe 100644 --- a/examples/resources/working_with_trait_versions/openassetio_example/specifications/example.py +++ b/examples/resources/working_with_trait_versions/openassetio_example/specifications/example.py @@ -59,12 +59,12 @@ def create(cls): data = TraitsData(cls.kTraitSet) return cls(data) - def deprectatedTrait(self): + def addedTrait(self): """ Returns the view for the 'openassetio-example:example.Unchanged' trait wrapped around the data held in this instance. """ - return traits.example.DeprecatedTrait_v1(self.traitsData()) + return traits.example.AddedTrait_v1(self.traitsData()) def updatedTrait(self): """ @@ -74,10 +74,6 @@ def updatedTrait(self): return traits.example.UpdatedTrait_v2(self.traitsData()) -# Alias for latest version. -ExampleSpecification = ExampleSpecification_v2 - - class ExampleSpecification_v1: """ An example. @@ -124,17 +120,20 @@ def create(cls): data = TraitsData(cls.kTraitSet) return cls(data) - - def deprectatedTrait(self): + def addedTrait(self): """ Returns the view for the 'openassetio-example:example.Unchanged' trait wrapped around the data held in this instance. """ - return traits.example.DeprecatedTrait_v1(self.traitsData()) + return traits.example.AddedTrait_v1(self.traitsData()) def updatedTrait(self): """ Returns the view for the 'openassetio-example:example.Updated' trait wrapped around the data held in this instance. """ - return traits.example.UpdatedTrait_v1(self.traitsData()) + return traits.example.UpdatedTrait_v2(self.traitsData()) + + +# Alias for first version. +ExampleSpecification = ExampleSpecification_v1 diff --git a/examples/resources/working_with_trait_versions/openassetio_example/traits/example.py b/examples/resources/working_with_trait_versions/openassetio_example/traits/example.py index b44fe59..ce96f6a 100644 --- a/examples/resources/working_with_trait_versions/openassetio_example/traits/example.py +++ b/examples/resources/working_with_trait_versions/openassetio_example/traits/example.py @@ -1,9 +1,9 @@ - """ Trait definitions in the 'example' namespace. Example namespace """ +import warnings # WARNING: This file is auto-generated by openassetio-traitgen, do not edit. @@ -17,7 +17,8 @@ class DeprecatedTrait_v1: An example. Usage: entity, locale, relationship """ - kId = "openassetio-example:example.Deprecated.v001" + + kId = "openassetio-example:example.Deprecated" __deprecated__ = True # (Eventually use PEP 702) @@ -67,16 +68,23 @@ def imbueTo(cls, traitsData): traitsData.addTrait(cls.kId) -# Alias for latest version. -DeprecatedTrait = DeprecatedTrait_v1 - +# Alias for first version. +class DeprecatedTrait(DeprecatedTrait_v1): + def __init__(self, traitsData): + super().__init__(traitsData) + warnings.warn( + "Use of unversioned trait view classes is deprecated", + category=DeprecationWarning, + stacklevel=2, + ) class UpdatedTrait_v2: """ An example. Usage: entity, locale, relationship """ - kId = "openassetio-example:example.Updated.v002" + + kId = "openassetio-example:example.Updated.v2" def __init__(self, traitsData): """ @@ -123,7 +131,6 @@ def imbueTo(cls, traitsData): """ traitsData.addTrait(cls.kId) - def setPropertyThatWasAdded(self, propertyThatWasAdded: float): """ Sets the propertyThatWasAdded property. @@ -134,7 +141,7 @@ def setPropertyThatWasAdded(self, propertyThatWasAdded: float): raise TypeError("propertyThatWasAdded must be a 'float'.") self.__data.setTraitProperty(self.kId, "propertyThatWasAdded", propertyThatWasAdded) - def getPropertyThatWasAdded(self, defaultValue: float=None) -> Union[float, None]: + def getPropertyThatWasAdded(self, defaultValue: float = None) -> Union[float, None]: """ Gets the value of the propertyThatWasAdded property or the supplied default. @@ -146,7 +153,9 @@ def getPropertyThatWasAdded(self, defaultValue: float=None) -> Union[float, None if not isinstance(value, float): if defaultValue is None: - raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'float'.") + raise TypeError( + f"Invalid stored value type: '{type(value).__name__}' should be 'float'." + ) return defaultValue return value @@ -160,7 +169,7 @@ def setPropertyThatWasRenamed(self, propertyThatWasRenamed: bool): raise TypeError("propertyThatWasRenamed must be a 'bool'.") self.__data.setTraitProperty(self.kId, "propertyThatWasRenamed", propertyThatWasRenamed) - def getPropertyThatWasRenamed(self, defaultValue: bool=None) -> Union[bool, None]: + def getPropertyThatWasRenamed(self, defaultValue: bool = None) -> Union[bool, None]: """ Gets the value of the propertyThatWasRenamed property or the supplied default. @@ -172,7 +181,9 @@ def getPropertyThatWasRenamed(self, defaultValue: bool=None) -> Union[bool, None if not isinstance(value, bool): if defaultValue is None: - raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'bool'.") + raise TypeError( + f"Invalid stored value type: '{type(value).__name__}' should be 'bool'." + ) return defaultValue return value @@ -186,7 +197,7 @@ def setPropertyToKeep(self, propertyToKeep: str): raise TypeError("propertyToKeep must be a 'str'.") self.__data.setTraitProperty(self.kId, "propertyToKeep", propertyToKeep) - def getPropertyToKeep(self, defaultValue: str=None) -> Union[str, None]: + def getPropertyToKeep(self, defaultValue: str = None) -> Union[str, None]: """ Gets the value of the propertyToKeep property or the supplied default. @@ -198,21 +209,20 @@ def getPropertyToKeep(self, defaultValue: str=None) -> Union[str, None]: if not isinstance(value, str): if defaultValue is None: - raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'str'.") + raise TypeError( + f"Invalid stored value type: '{type(value).__name__}' should be 'str'." + ) return defaultValue return value -# Alias to latest version -UpdatedTrait = UpdatedTrait_v2 - - class UpdatedTrait_v1: """ An example. Usage: entity, locale, relationship """ - kId = "openassetio-example:example.Updated.v001" + + kId = "openassetio-example:example.Updated" def __init__(self, traitsData): """ @@ -259,7 +269,6 @@ def imbueTo(cls, traitsData): """ traitsData.addTrait(cls.kId) - def setPropertyToKeep(self, propertyToKeep: str): """ Sets the propertyToKeep property. @@ -270,7 +279,7 @@ def setPropertyToKeep(self, propertyToKeep: str): raise TypeError("propertyToKeep must be a 'str'.") self.__data.setTraitProperty(self.kId, "propertyToKeep", propertyToKeep) - def getPropertyToKeep(self, defaultValue: str=None) -> Union[str, None]: + def getPropertyToKeep(self, defaultValue: str = None) -> Union[str, None]: """ Gets the value of the propertyToKeep property or the supplied default. @@ -282,7 +291,9 @@ def getPropertyToKeep(self, defaultValue: str=None) -> Union[str, None]: if not isinstance(value, str): if defaultValue is None: - raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'str'.") + raise TypeError( + f"Invalid stored value type: '{type(value).__name__}' should be 'str'." + ) return defaultValue return value @@ -296,7 +307,7 @@ def setPropertyToRemove(self, propertyToRemove: bool): raise TypeError("propertyToRemove must be a 'bool'.") self.__data.setTraitProperty(self.kId, "propertyToRemove", propertyToRemove) - def getPropertyToRemove(self, defaultValue: bool=None) -> Union[bool, None]: + def getPropertyToRemove(self, defaultValue: bool = None) -> Union[bool, None]: """ Gets the value of the propertyToRemove property or the supplied default. @@ -308,7 +319,9 @@ def getPropertyToRemove(self, defaultValue: bool=None) -> Union[bool, None]: if not isinstance(value, bool): if defaultValue is None: - raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'bool'.") + raise TypeError( + f"Invalid stored value type: '{type(value).__name__}' should be 'bool'." + ) return defaultValue return value @@ -323,7 +336,7 @@ def setPropertyToRename(self, propertyToRename: bool): raise TypeError("propertyToRename must be a 'bool'.") self.__data.setTraitProperty(self.kId, "propertyToRename", propertyToRename) - def getPropertyToRename(self, defaultValue: bool=None) -> Union[bool, None]: + def getPropertyToRename(self, defaultValue: bool = None) -> Union[bool, None]: """ Gets the value of the propertyToRename property or the supplied default. @@ -336,17 +349,30 @@ def getPropertyToRename(self, defaultValue: bool=None) -> Union[bool, None]: if not isinstance(value, bool): if defaultValue is None: - raise TypeError(f"Invalid stored value type: '{type(value).__name__}' should be 'bool'.") + raise TypeError( + f"Invalid stored value type: '{type(value).__name__}' should be 'bool'." + ) return defaultValue return value +# Alias to first version +class UpdatedTrait(UpdatedTrait_v1): + def __init__(self, traitsData): + super().__init__(traitsData) + warnings.warn( + "Use of unversioned trait view classes is deprecated", + category=DeprecationWarning, + stacklevel=2, + ) + class AddedTrait_v1: """ An example. Usage: entity, locale, relationship """ - kId = "openassetio-example:example.Added.v001" + + kId = "openassetio-example:example.Added" def __init__(self, traitsData): """ @@ -394,5 +420,12 @@ def imbueTo(cls, traitsData): traitsData.addTrait(cls.kId) -# Alias to latest version -AddedTrait = AddedTrait_v1 \ No newline at end of file +# Alias to first version +class AddedTrait(AddedTrait_v1): + def __init__(self, traitsData): + super().__init__(traitsData) + warnings.warn( + "Use of unversioned trait view classes is deprecated", + category=DeprecationWarning, + stacklevel=2, + ) diff --git a/examples/working_with_trait_versions.ipynb b/examples/working_with_trait_versions.ipynb index cad22cf..1bf6c11 100644 --- a/examples/working_with_trait_versions.ipynb +++ b/examples/working_with_trait_versions.ipynb @@ -39,8 +39,8 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-05-07T12:30:33.824668Z", - "start_time": "2024-05-07T12:30:33.814780Z" + "end_time": "2024-05-16T13:20:48.919285Z", + "start_time": "2024-05-16T13:20:48.906980Z" } }, "id": "ed3207872eaa89b0", @@ -54,7 +54,9 @@ "\n", "We imagine an industry where there are only 3 traits, `AddedTrait`, `DeprecatedTrait`, and `UpdatedTrait`, which are used across entities, relationships, policies and locales. The traits themselves have no meaning, they are named purely to give a hint as to how they are versioned.\n", "\n", - "Within the package, trait views for all previous versions are included, with a version suffix on the class name. The class name without a version suffix is an alias to the latest version available in the package.\n", + "Within the package, trait views for all previous versions are included, with a version suffix on the class name. As a special case for backward compatibility, the class name without a version suffix is an alias to version 1.\n", + "\n", + "The unique ID of a trait will compare non-equal between different versions of the same trait.\n", "\n", "Traits can be marked as deprecated to signal that they will eventually be removed from the schema altogether." ], @@ -66,19 +68,16 @@ { "cell_type": "code", "source": [ - "assert traits.example.UpdatedTrait is traits.example.UpdatedTrait_v2\n", - "assert traits.example.UpdatedTrait is not traits.example.UpdatedTrait_v1\n", - "\n", - "assert traits.example.AddedTrait is traits.example.AddedTrait_v1\n", + "assert traits.example.UpdatedTrait_v1.kId == traits.example.UpdatedTrait.kId\n", + "assert traits.example.UpdatedTrait_v2.kId != traits.example.UpdatedTrait.kId\n", "\n", - "assert traits.example.DeprecatedTrait is traits.example.DeprecatedTrait_v1\n", - "assert traits.example.DeprecatedTrait_v1.__deprecated__" + "assert traits.example.DeprecatedTrait_v1.__deprecated__\n" ], "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-05-07T12:30:33.841243Z", - "start_time": "2024-05-07T12:30:33.825997Z" + "end_time": "2024-05-16T13:20:49.017184Z", + "start_time": "2024-05-16T13:20:49.014169Z" } }, "id": "1470d16817a82421", @@ -86,22 +85,46 @@ "execution_count": 2 }, { + "metadata": {}, "cell_type": "markdown", + "source": "The non-suffixed classes are deprecated and should not be used. Using these will give a warning.", + "id": "574a603365c048c0" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-05-16T13:20:49.039103Z", + "start_time": "2024-05-16T13:20:49.036043Z" + } + }, + "cell_type": "code", "source": [ - "Host applications that bundle the trait package may use the non-version-suffixed classes by default.\n", - " \n", - "For Python manager plugins, if\n", + "import warnings\n", + "warnings.simplefilter(\"default\")\n", "\n", - "* the host application's largest (most-recent) set of versions does not match the versions the Python plugin was developed against; and\n", - "* the Python plugin uses the default (non-suffixed) traits/specifications classes\n", - " \n", - "the application may hit unexpected runtime errors. This is because the structure of trait/specification view classes may be different, or simply not exist.\n", + "from openassetio.trait import TraitsData\n", "\n", - "A Python manager plugin should therefore use version-suffixed classses. This solves the problem when the plugin is loaded into an application that defaults to a _higher_ set of versions than the plugin was developed for.\n", - " \n", - "However, if the host application's largest available versions are too _low_ for the manager plugin, then using a version-suffixed class would suffer an `ImportError` when attempting to `import` the (too new) versioned class. This can either be left to fail (better to fail early), or be tolerated within the plugin by catching the exception and falling back to an older version\n", + "_ = traits.example.AddedTrait(TraitsData())" + ], + "id": "8a77383d0cd2a877", + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_298107/3386987461.py:6: DeprecationWarning: Use of unversioned trait view classes is deprecated\n", + " _ = traits.example.AddedTrait(TraitsData())\n" + ] + } + ], + "execution_count": 3 + }, + { + "cell_type": "markdown", + "source": [ + "In a Python environment, if the host application's largest available versions are too _low_ for the manager plugin, then using a version-suffixed class would suffer an `ImportError` when attempting to `import` the (too new) versioned class. This can either be left to fail (better to fail early), or be tolerated within the plugin by catching the exception and falling back to an older version\n", " \n", - "C++ plugins have the trait view classes (privately) compiled into them, so do not depend on the versions that the host application was built against/ships with. Incompatibilities only become apparent at runtime, when incoming trait data is found to be of a version unsupported by the manager or host." + "C++ plugins have the trait view classes (privately) compiled into them, so do not depend on the versions that the host application was built against/ships with. Incompatibilities only become apparent at runtime in the data layer, when incoming trait data is found to be of a version unsupported by the manager or host." ], "metadata": { "collapsed": false @@ -114,7 +137,7 @@ "source": [ "### Trait IDs\n", "\n", - "Each trait encodes its version in its unique ID, up to a maximum version of 999, and zero-padded, such that `kId[:-5]` can be used to strip the version tag." + "Each trait at version 2 or above encodes its version in its unique ID. As a special case, version 1 has no version tag." ], "id": "59b7afb066b5bcc9" }, @@ -122,22 +145,22 @@ "cell_type": "code", "source": [ "# An updated trait has a different ID for each version.\n", - "assert traits.example.UpdatedTrait_v1.kId == \"openassetio-example:example.Updated.v001\"\n", - "assert traits.example.UpdatedTrait_v2.kId == \"openassetio-example:example.Updated.v002\"\n", + "assert traits.example.UpdatedTrait_v1.kId == \"openassetio-example:example.Updated\"\n", + "assert traits.example.UpdatedTrait_v2.kId == \"openassetio-example:example.Updated.v2\"\n", "\n", - "# A newly added trait starts at version 1.\n", - "assert traits.example.AddedTrait.kId == \"openassetio-example:example.Added.v001\"" + "# A newly added trait starts at version 1, which as no version tag in the ID.\n", + "assert traits.example.AddedTrait.kId == \"openassetio-example:example.Added\"" ], "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-05-07T12:30:33.857952Z", - "start_time": "2024-05-07T12:30:33.842191Z" + "end_time": "2024-05-16T13:20:49.057577Z", + "start_time": "2024-05-16T13:20:49.054304Z" } }, "id": "a31f8a7263370714", "outputs": [], - "execution_count": 3 + "execution_count": 4 }, { "cell_type": "markdown", @@ -157,20 +180,20 @@ "\n", "traits.example.UpdatedTrait_v1.imbueTo(v1_data)\n", "\n", - "assert traits.example.UpdatedTrait.isImbuedTo(v1_data) is False\n", + "assert traits.example.UpdatedTrait.isImbuedTo(v1_data) is True\n", "assert traits.example.UpdatedTrait_v1.isImbuedTo(v1_data) is True\n", "assert traits.example.UpdatedTrait_v2.isImbuedTo(v1_data) is False\n" ], "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-05-07T12:30:33.870856Z", - "start_time": "2024-05-07T12:30:33.858926Z" + "end_time": "2024-05-16T13:20:49.085065Z", + "start_time": "2024-05-16T13:20:49.081220Z" } }, "id": "a147006dd4b9b453", "outputs": [], - "execution_count": 4 + "execution_count": 5 }, { "metadata": {}, @@ -178,21 +201,46 @@ "source": [ "### Specifications\n", "\n", - "Specifications are a way to document well-known sets of traits that categorize entities, relationships, locales, or policies. Agreement on these as an industry is crucial for effective interop.\n", + "Specifications are a way to document well-known sets of traits that categorize entities, relationships, locales, or policies. Agreement on these as an industry is crucial for effective interop. View classes are generated for these, which compose the trait views of their trait set.\n", "\n", + "Similar to traits, the class names have a version tag suffixed to them, and the untagged class is a deprecated alias to version 1. \n", + "\n" + ], + "id": "ab4dde470a3808d4" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-05-16T13:20:49.124614Z", + "start_time": "2024-05-16T13:20:49.121773Z" + } + }, + "cell_type": "code", + "source": [ + "assert specifications.example.ExampleSpecification_v1.kTraitSet == specifications.example.ExampleSpecification.kTraitSet\n", + "assert specifications.example.ExampleSpecification_v2.kTraitSet != specifications.example.ExampleSpecification.kTraitSet" + ], + "id": "220587e7fdcc6e1f", + "outputs": [], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ "However, no specification version is embedded in the data itself. The data sent between host and manager only contains the versions of the composite traits. Detecting that some trait data satisfies a specification therefore requires a try-and-see approach, checking the data against the specification's trait set.\n", "\n", - "If the raison d'être of specifications is a way to have an industry-standard collection of trait sets, then is the exact version of those traits really important? On the assumption that a new version of a trait doesn't fundamentally change its meaning (otherwise it would be an entirely new trait), then it's reasonable to say that specifications are trait version agnostic.\n", + "If specifications are a way to have an industry-standard collection of trait sets, then is the exact version of those traits really important? On the assumption that a new version of a trait doesn't fundamentally change its meaning (otherwise it would be an entirely new trait), then it's reasonable to say that specifications are trait version agnostic.\n", "\n", "So specifications are invaluable as documentation of common trait sets. However, the auto-generateed `Specification` view classes should be used with caution: using a specification view class to categorize an entity may lead to unexpected false negatives when the trait versions do not line up.\n" ], - "id": "ab4dde470a3808d4" + "id": "5a8eabe3cd9e7a3a" }, { "cell_type": "code", "source": [ "entity_data = TraitsData(\n", - " {traits.example.AddedTrait.kId, traits.example.UpdatedTrait_v1.kId})\n", + " {traits.example.AddedTrait_v1.kId, traits.example.UpdatedTrait_v1.kId})\n", "\n", "# As long as the trait set of the specification lines up with the\n", "# incoming data, we can use it to categorize an entity.\n", @@ -204,20 +252,20 @@ "# to the specification mean the version suffix on some trait IDs have\n", "# updated and no longer match the incoming data, so we get a false\n", "# negative.\n", - "is_an_example_entity = specifications.example.ExampleSpecification.kTraitSet.issubset(\n", - " entity_data.traitSet()) # or ExampleSpecification_v2\n", + "is_an_example_entity = specifications.example.ExampleSpecification_v2.kTraitSet.issubset(\n", + " entity_data.traitSet())\n", "assert is_an_example_entity is False\n" ], "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-05-07T12:30:33.887271Z", - "start_time": "2024-05-07T12:30:33.872435Z" + "end_time": "2024-05-16T13:20:49.150228Z", + "start_time": "2024-05-16T13:20:49.146420Z" } }, "id": "a94f4bcd14862e3a", "outputs": [], - "execution_count": 5 + "execution_count": 7 }, { "cell_type": "markdown", @@ -256,13 +304,13 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-05-07T12:30:33.904152Z", - "start_time": "2024-05-07T12:30:33.890148Z" + "end_time": "2024-05-16T13:20:49.229661Z", + "start_time": "2024-05-16T13:20:49.225468Z" } }, "id": "592a08fb134655a", "outputs": [], - "execution_count": 6 + "execution_count": 8 }, { "cell_type": "markdown", @@ -323,7 +371,7 @@ "\n", " # Assume we know from reading release notes that UnchangedTrait\n", " # hasn't changed, so we can use v2 knowing that v1 is equivalent.\n", - " if traits.example.AddedTrait.isImbuedTo(context.locale):\n", + " if traits.example.AddedTrait_v1.isImbuedTo(context.locale):\n", " # UpdatedTrait changes between versions, but we know from\n", " # release notes that the property we're interested in still\n", " # exists semantically, it's just the name has changed.\n", @@ -399,8 +447,8 @@ " for idx, trait_set in enumerate(traitSets):\n", " is_an_updated = (traits.example.UpdatedTrait_v2.kId in trait_set or\n", " traits.example.UpdatedTrait_v1.kId in trait_set)\n", - " is_a_deprecated = traits.example.DeprecatedTrait.kId in trait_set\n", - " is_an_added = traits.example.AddedTrait.kId in trait_set\n", + " is_a_deprecated = traits.example.DeprecatedTrait_v1.kId in trait_set\n", + " is_an_added = traits.example.AddedTrait_v1.kId in trait_set\n", "\n", " if is_an_updated and is_a_deprecated:\n", " entity_ref = \"example://default/deprecated\"\n", @@ -443,11 +491,11 @@ " # an entity as an Example. No way to know what\n", " # version the host would prefer. So prefer default,\n", " # which will be the latest, v2.\n", - " successCallback(idx, specifications.example.ExampleSpecification.kTraitSet)\n", + " successCallback(idx, specifications.example.ExampleSpecification_v2.kTraitSet)\n", " else:\n", " # Minimum required for publishing is a reduced set.\n", " successCallback(\n", - " idx, specifications.example.ExampleSpecification.kTraitSet - {\n", + " idx, specifications.example.ExampleSpecification_v2.kTraitSet - {\n", " traits.example.AddedTrait.kId})\n", "\n", " else:\n", @@ -485,14 +533,14 @@ " elif resolveAccess == access.ResolveAccess.kManagerDriven:\n", " # Only support v2 for publishing workflows.\n", " if traits.example.UpdatedTrait_v2.kId in traitSet:\n", - " trait = traits.example.UpdatedTrait_v1(traits_data)\n", + " trait = traits.example.UpdatedTrait_v2(traits_data)\n", " trait.setPropertyThatWasAdded(456.789)\n", " else:\n", " # Similar for other entities.\n", " if resolveAccess == access.ResolveAccess.kManagerDriven:\n", " # Only support latest version (v2) for publishing workflows.\n", - " if traits.example.UpdatedTrait.kId in traitSet:\n", - " trait = traits.example.UpdatedTrait(traits_data)\n", + " if traits.example.UpdatedTrait_v2.kId in traitSet:\n", + " trait = traits.example.UpdatedTrait_v2(traits_data)\n", " trait.setPropertyThatWasAdded(789.123)\n", " trait.setPropertyThatWasRenamed(True)\n", "\n", @@ -512,9 +560,9 @@ "\n", " # Parse out important aspects of the type of relationship.\n", "\n", - " is_rel_added = traits.example.AddedTrait.isimbuedTo(\n", + " is_rel_added = traits.example.AddedTrait_v1.isimbuedTo(\n", " relationshipTraitsData)\n", - " is_rel_deprecated = traits.example.DeprecatedTrait.isimbuedTo(\n", + " is_rel_deprecated = traits.example.DeprecatedTrait_v1.isimbuedTo(\n", " relationshipTraitsData)\n", "\n", " rel_v2_updated = traits.example.UpdatedTrait_v2(relationshipTraitsData)\n", @@ -615,8 +663,8 @@ " # Categorise the data to publish\n", " is_an_updated = (traits.example.UpdatedTrait_v2.isImbuedTo(traits_data) or\n", " traits.example.UpdatedTrait_v1.isImbuedTo(traits_data))\n", - " is_a_deprecated = traits.example.DeprecatedTrait.isImbuedTo(traits_data)\n", - " is_an_added = traits.example.AddedTrait.isImbuedTo(traits_data)\n", + " is_a_deprecated = traits.example.DeprecatedTrait_v1.isImbuedTo(traits_data)\n", + " is_an_added = traits.example.AddedTrait_v1.isImbuedTo(traits_data)\n", "\n", " # Based on the given traits, categorize to pipeline-specific \"type\"\n", " is_an_item = is_an_added and is_an_updated\n", @@ -672,7 +720,7 @@ " is_an_updated = (traits.example.UpdatedTrait_v2.isImbuedTo(traits_data) or\n", " traits.example.UpdatedTrait_v1.isImbuedTo(traits_data))\n", " is_a_deprecated = traits.example.DeprecatedTrait.isImbuedTo(traits_data)\n", - " is_an_added = traits.example.AddedTrait.isImbuedTo(traits_data)\n", + " is_an_added = traits.example.AddedTrait_v1.isImbuedTo(traits_data)\n", "\n", " # Based on the given traits, categorize to pipeline-specific, \"type\"\n", " is_an_item = is_an_added and is_an_updated\n", @@ -749,13 +797,13 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-05-07T12:30:33.946754Z", - "start_time": "2024-05-07T12:30:33.906750Z" + "end_time": "2024-05-16T13:20:49.302536Z", + "start_time": "2024-05-16T13:20:49.248027Z" } }, "id": "5bb5454ca1ae8b06", "outputs": [], - "execution_count": 7 + "execution_count": 9 }, { "cell_type": "markdown", @@ -782,7 +830,7 @@ "\n", "# Configure the locale\n", "\n", - "context.locale.addTraits(specifications.example.ExampleSpecification.kTraitSet)\n", + "context.locale.addTraits(specifications.example.ExampleSpecification_v2.kTraitSet)\n", "\n", "# The minimum set of traits required to publish to this entity\n", "# reference.\n", @@ -790,7 +838,7 @@ "\n", "# Whatever the minimum trait set is, we know we're going to publish an\n", "# Example.\n", - "desired_trait_set = minimum_trait_set | specifications.example.ExampleSpecification.kTraitSet\n", + "desired_trait_set = minimum_trait_set | specifications.example.ExampleSpecification_v2.kTraitSet\n", "\n", "# Get the set of traits that have properties the manager can persist.\n", "[policy_for_desired_traits] = manager.managementPolicy(\n", @@ -893,18 +941,18 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-05-07T12:30:33.963775Z", - "start_time": "2024-05-07T12:30:33.947772Z" + "end_time": "2024-05-16T13:20:49.321308Z", + "start_time": "2024-05-16T13:20:49.309670Z" } }, "id": "b8e12c55d88bda27", "outputs": [], - "execution_count": 8 + "execution_count": 10 }, { "cell_type": "markdown", "source": [ - "## Conclusion\n", + "## Future work\n", "\n", "The above explorations have shown that working with versioned traits is entirely possible using the existing API.\n", "\n",