Skip to content

resource_service.py

ofrak.service.resource_service

AttributeIndexDict (defaultdict)

ResourceService (ResourceServiceInterface)

create(self, resource) async

Add a ResourceModel to the resource service database according to the given model. If the resource model says it has a parent, resource will be added as a child of that parent.

Parameters:

Name Type Description Default
resource ResourceModel

The resource model to add to the database

required

Returns:

Type Description
ResourceModel

The same model which was passed in, with no changes

Exceptions:

Type Description
AlreadyExistError

If resource has an ID which already exists in the database

NotFoundError

If resource has a parent ID but no resource with that ID exists

Source code in ofrak/service/resource_service.py
async def create(self, resource: ResourceModel) -> ResourceModel:
    if resource.id in self._resource_store:
        raise AlreadyExistError(f"A resource with id {resource.id.hex()} already exists!")
    if resource.parent_id is not None:
        parent_resource_node = self._resource_store.get(resource.parent_id)
        if parent_resource_node is None:
            raise NotFoundError(
                f"The parent resource with id {resource.parent_id.hex()} does not exist"
            )
        LOGGER.debug(
            f"Creating resource {resource.id.hex()} as child of {resource.parent_id.hex()}"
        )
    else:
        parent_resource_node = None
        LOGGER.debug(f"Creating resource {resource.id.hex()}")
    resource_node = ResourceNode(resource, parent_resource_node)
    self._resource_store[resource.id] = resource_node
    if resource.data_id is not None:
        self._resource_by_data_id_store[resource.data_id] = resource_node
    if parent_resource_node is None:
        self._root_resources[resource.id] = resource_node

    # Take care of the indexes
    for tag in resource.tags:
        self._add_resource_tag_to_index(tag, resource_node)
    for indexable_attribute, value in resource.get_index_values().items():
        self._add_resource_attribute_to_index(indexable_attribute, value, resource_node)
    return resource

get_root_resources(self) async

Get all of the root resources known to this resource service. Any resource created without a parent will be returned by this method.

Returns:

Type Description
List[ofrak.model.resource_model.ResourceModel]

All resources with no parents

Source code in ofrak/service/resource_service.py
async def get_root_resources(self) -> List[ResourceModel]:
    return [root_node.model for root_node in self._root_resources.values()]

verify_ids_exist(self, resource_ids) async

Check if a number of resource IDs exist in the resource store. This is useful for filtering out IDs of resources which have been deleted.

Parameters:

Name Type Description Default
resource_ids Iterable[bytes]

Iterable of resource IDs to check for

required

Returns:

Type Description
Iterable[bool]

A boolean for each resource ID, True if it exists in the store and False otherwise

Source code in ofrak/service/resource_service.py
async def verify_ids_exist(self, resource_ids: Iterable[bytes]) -> Iterable[bool]:
    return [resource_id in self._resource_store for resource_id in resource_ids]

get_by_data_ids(self, data_ids) async

Get the resource models with a given sequence of data IDs.

Parameters:

Name Type Description Default
data_ids Iterable[bytes]

A list of valid data IDs

required

Returns:

Type Description
Iterable[ofrak.model.resource_model.ResourceModel]

A sequence of resource models each with one of the given data IDs, in the same order which data_ids was given in.

Exceptions:

Type Description
NotFoundError

If there is not a resource for all of the IDs in data_ids

Source code in ofrak/service/resource_service.py
async def get_by_data_ids(self, data_ids: Iterable[bytes]) -> Iterable[ResourceModel]:
    results = []
    for data_id in data_ids:
        resource_node = self._resource_by_data_id_store.get(data_id)
        if resource_node is None:
            raise NotFoundError(f"The resource with data ID {data_id.hex()} does not exist")
        results.append(resource_node.model)
    return results

get_by_ids(self, resource_ids) async

Get the resource models with a given sequence of resource IDs.

Parameters:

Name Type Description Default
resource_ids Iterable[bytes]

A list of valid resource IDs

required

Returns:

Type Description
Iterable[ofrak.model.resource_model.ResourceModel]

A sequence of resource models each with one of the given resource IDs, in the same order which resource_ids was given in.

Exceptions:

Type Description
NotFoundError

If there is not a resource for all of the IDs in resource_ids

Source code in ofrak/service/resource_service.py
async def get_by_ids(self, resource_ids: Iterable[bytes]) -> Iterable[ResourceModel]:
    results = []
    for resource_id in resource_ids:
        resource_node = self._resource_store.get(resource_id)
        if resource_node is None:
            raise NotFoundError(f"The resource {resource_id.hex()} does not exist")
        results.append(resource_node.model)
    return results

get_by_id(self, resource_id) async

Get the resource model with a given resource ID.

Parameters:

Name Type Description Default
resource_id bytes

A valid resource ID

required

Returns:

Type Description
ResourceModel

The resource model with ID matching resource_id

Exceptions:

Type Description
NotFoundError

If there is not a resource with resource ID resource_id

Source code in ofrak/service/resource_service.py
async def get_by_id(self, resource_id: bytes) -> ResourceModel:
    LOGGER.debug(f"Fetching resource {resource_id.hex()}")
    resource_node = self._resource_store.get(resource_id)
    if resource_node is None:
        raise NotFoundError(f"The resource {resource_id.hex()} does not exist")
    return resource_node.model

get_depths(self, resource_ids) async

Get the depth of each resource in resource_ids.

Parameters:

Name Type Description Default
resource_ids Iterable[bytes]

A list of valid resource IDs

required

Returns:

Type Description
Iterable[int]

A sequence of resource model depths, in the same order which resource_ids was given in.

Exceptions:

Type Description
NotFoundError

If there is not a resource for all of the IDs in resource_ids

Source code in ofrak/service/resource_service.py
async def get_depths(self, resource_ids: Iterable[bytes]) -> Iterable[int]:
    results = []
    for resource_id in resource_ids:
        resource_node = self._resource_store.get(resource_id)
        if resource_node is None:
            raise NotFoundError(f"The resource {resource_id.hex()} does not exist")
        results.append(resource_node.get_depth())
    return results

get_ancestors_by_id(self, resource_id, max_count=-1, r_filter=None) async

Get the resource models of the ancestors of a resource with a given ID. These ancestors may be filtered by an optional filter argument. A maximum count of ancestors may also be given, to cap the number of (filtered or unfiltered) ancestors returned.

Parameters:

Name Type Description Default
resource_id bytes

ID of resource to get ancestors of

required
max_count int

Optional argument to cap the number of models returned; if set to -1 (default) then any number of ancestors may be returned

-1
r_filter Optional[ofrak.service.resource_service_i.ResourceFilter]

Optional resource filter for the resource models returned; if set to None, all ancestors may be returned (the model for resource_id is excluded), otherwise all ancestors matching the filter may be returned (possibly including the model for resource_id), up to the maximum allowed by max_count

None

Returns:

Type Description
Iterable[ofrak.model.resource_model.ResourceModel]

As many ancestors of resource_id matching r_filter as max_count allows, in order of reverse depth (deeper resources first, root last)

Exceptions:

Type Description
NotFoundError

If there is not a resource with resource ID resource_id

Source code in ofrak/service/resource_service.py
async def get_ancestors_by_id(
    self,
    resource_id: bytes,
    max_count: int = -1,
    r_filter: Optional[ResourceFilter] = None,
) -> Iterable[ResourceModel]:
    LOGGER.debug(f"Fetching ancestor(s) of {resource_id.hex()}")
    resource_node = self._resource_store.get(resource_id)
    if resource_node is None:
        raise NotFoundError(f"The resource {resource_id.hex()} does not exist")
    r_filter_logic = AggregateResourceFilterLogic.create(
        r_filter,
        self._tag_indexes,
        self._attribute_indexes,
    )
    include_root = False if r_filter is None else r_filter.include_self
    resources = map(
        lambda n: n.model,
        filter(r_filter_logic.filter, resource_node.walk_ancestors(include_root)),
    )
    if max_count < 0:
        return resources
    return itertools.islice(resources, 0, max_count)

get_descendants_by_id(self, resource_id, max_count=-1, max_depth=-1, r_filter=None, r_sort=None) async

Get the resource models of the descendants of a resource with a given ID. These descendants may be filtered by an optional filter argument. A maximum count of descendants may also be given, to cap the number of (filtered or unfiltered) descendants returned. A maximum depth may also be given, to limit how deep to search for descendants.

Parameters:

Name Type Description Default
resource_id bytes

ID of resource to get descendants of

required
max_count int

Optional argument to cap the number of models returned; if set to -1 (default) then any number of descendants may be returned

-1
max_depth int

Optional argument to limit the depth to search for descendants; if set to -1 (default) then descendants of any depth may be returned; if set to 1, for example, only children of resource_id may be returned

-1
r_filter Optional[ofrak.service.resource_service_i.ResourceFilter]

Optional resource filter for the resource models returned; if set to None all descendants may be returned (the model for resource_id is excluded), otherwise all descendants matching the filter may be returned (possibly including the model for resource_id), up to the maximum allowed by max_count

None
r_sort Optional[ofrak.service.resource_service_i.ResourceSort]

Optional logic to order the returned descendants by the value of a specific attribute of each descendant

None

Returns:

Type Description
Iterable[ofrak.model.resource_model.ResourceModel]

As many descendants of resource_id matching r_filter as max_count allows, in order specified by r_sort. If r_sort is None, no specific ordering is guaranteed.

Exceptions:

Type Description
NotFoundError

If there is not a resource with resource ID resource_id

Source code in ofrak/service/resource_service.py
async def get_descendants_by_id(
    self,
    resource_id: bytes,
    max_count: int = -1,
    max_depth: int = -1,
    r_filter: Optional[ResourceFilter] = None,
    r_sort: Optional[ResourceSort] = None,
) -> Iterable[ResourceModel]:
    # LOGGER.debug(f"Fetching descendant(s) of {resource_id.hex()}")
    resource_node = self._resource_store.get(resource_id)
    if resource_node is None:
        raise NotFoundError(f"The resource {resource_id.hex()} does not exist")

    aggregate_sort_logic = ResourceSortLogic.create(r_sort, self._attribute_indexes)
    aggregate_filter_logic = AggregateResourceFilterLogic.create(
        r_filter, self._tag_indexes, self._attribute_indexes, resource_node, max_depth
    )
    # This is the planning phase used to determine the best index to use for further filtering
    filter_logic: Optional[ResourceFilterLogic] = None
    filter_cost = sys.maxsize
    sort_cost = sys.maxsize
    if aggregate_sort_logic.has_effect():
        sort_cost = aggregate_sort_logic.get_match_count()
        if sort_cost == 0:
            return tuple()

    for _filter_logic in aggregate_filter_logic.filters:
        _filter_cost = _filter_logic.get_match_count()
        if _filter_cost == 0:
            return tuple()
        if (
            aggregate_sort_logic.has_effect()
            and aggregate_sort_logic.get_attribute() != _filter_logic.get_attribute()
        ):
            # The resources matching the filter would need to get sorted, making the
            # worst case # scenario more expensive
            _filter_cost = int(_filter_cost * math.log2(_filter_cost))
        if _filter_cost < filter_cost:
            filter_cost = _filter_cost
            filter_logic = _filter_logic

    # Use the estimated cost to pick the fastest way to compute the results
    if (
        filter_logic is not None
        and filter_logic.get_attribute() is not None
        and filter_logic.get_attribute() == aggregate_sort_logic.get_attribute()
    ):
        resource_nodes = filter_logic.walk(aggregate_sort_logic.get_direction())
        aggregate_filter_logic.ignore_filter(filter_logic)
        aggregate_sort_logic = NullResourceSortLogic()
    elif sort_cost < filter_cost:
        resource_nodes = aggregate_sort_logic.walk()
        aggregate_sort_logic = NullResourceSortLogic()
    elif filter_logic is not None:
        resource_nodes = filter_logic.walk(ResourceSortDirection.ASCENDANT)
        # No need to filter on that index since it serves as the root index
        aggregate_filter_logic.ignore_filter(filter_logic)

    if aggregate_filter_logic.has_effect():
        resource_nodes = filter(aggregate_filter_logic.filter, resource_nodes)
    resources: Iterable[ResourceModel] = map(lambda n: n.model, resource_nodes)
    if aggregate_sort_logic.has_effect():
        resources = aggregate_sort_logic.sort(resources)
    if max_count >= 0:
        resources = itertools.islice(resources, 0, max_count)
    return resources

get_siblings_by_id(self, resource_id, max_count=-1, r_filter=None, r_sort=None) async

Get the resource models of the siblings of a resource with a given ID. These siblings may be filtered by an optional filter argument. A maximum count of siblings may also be given, to cap the number of (filtered or unfiltered) siblings returned.

Parameters:

Name Type Description Default
resource_id bytes

ID of resource to get siblings of

required
max_count int

Optional argument to cap the number of models returned; if set to -1 (default) then any number of siblings may be returned

-1
r_filter Optional[ofrak.service.resource_service_i.ResourceFilter]

Optional resource filter for the resource models returned; if set to None all siblings may be returned (the model for resource_id is excluded), otherwise all siblings matching the filter may be returned (possibly including the model for resource_id), up to the maximum allowed by max_count

None
r_sort Optional[ofrak.service.resource_service_i.ResourceSort]

Optional logic to order the returned siblings by the value of a specific attribute of each sibling

None

Returns:

Type Description
Iterable[ofrak.model.resource_model.ResourceModel]

As many siblings of resource_id matching r_filter as max_count allows, in order specified by r_sort. If r_sort is None, no specific ordering is guaranteed.

Exceptions:

Type Description
NotFoundError

If there is not a resource with resource ID resource_id

NotFoundError

If the resource with ID resource_id does not have siblings because it is a root

Source code in ofrak/service/resource_service.py
async def get_siblings_by_id(
    self,
    resource_id: bytes,
    max_count: int = -1,
    r_filter: Optional[ResourceFilter] = None,
    r_sort: Optional[ResourceSort] = None,
) -> Iterable[ResourceModel]:
    resource_node = self._resource_store.get(resource_id)
    if resource_node is None:
        raise NotFoundError(f"The resource {resource_id.hex()} does not exist")
    if resource_node.parent is None:
        raise NotFoundError(
            f"The resource {resource_id.hex()} does not have siblings as it is a root "
            f"resource."
        )
    return await self.get_descendants_by_id(
        resource_node.parent.model.id, max_count, 1, r_filter, r_sort
    )

update(self, resource_diff) async

Modify a stored resource model according to the differences in the given diff object.

Parameters:

Name Type Description Default
resource_diff ResourceModelDiff

Diff object containing changes to a resource model, as well as the resource ID of the model to update

required

Returns:

Type Description
ResourceModel

The updated resource model (with changes applied)

Exceptions:

Type Description
NotFoundError

If there is not a resource with resource ID matching the ID in resource_diff

Source code in ofrak/service/resource_service.py
async def update(self, resource_diff: ResourceModelDiff) -> ResourceModel:
    LOGGER.debug(f"Saving resource {resource_diff.id.hex()}")
    resource_node = self._resource_store.get(resource_diff.id)
    if resource_node is None:
        raise NotFoundError(f"The resource with ID {resource_diff.id.hex()} does not exist")

    prev_resource = resource_node.model
    next_resource = resource_diff.apply(prev_resource)

    current_tags = next_resource.get_tags()
    # Update the tag indexes
    for tag_removed in resource_diff.tags_removed:
        self._remove_resource_tag_from_index(tag_removed, resource_node, set(current_tags))
    for tag_added in resource_diff.tags_added:
        self._add_resource_tag_to_index(tag_added, resource_node)

    # Update the attribute indexes
    indexable_attributes_removed = set()
    for attributes_removed in resource_diff.attributes_removed:
        for indexable_attribute_removed in attributes_removed.get_indexable_attributes():
            indexable_attributes_removed.add(indexable_attribute_removed)
            self._remove_resource_attribute_from_index(
                indexable_attribute_removed, resource_node
            )
    indexable_attrs_indirectly_removed = next_resource.get_index_values_depending_on_indexes(
        indexable_attributes_removed
    )
    for indexable_attr_indirectly_removed in indexable_attrs_indirectly_removed.keys():
        self._remove_resource_attribute_from_index(
            indexable_attr_indirectly_removed, resource_node
        )

    indexable_attributes_added = set()
    for attributes_type_added, attributes_added in resource_diff.attributes_added.items():
        for indexable_attribute_added in attributes_added.get_indexable_attributes():
            indexable_attributes_added.add(indexable_attribute_added)
            self._add_resource_attribute_to_index(
                indexable_attribute_added,
                indexable_attribute_added.get_value(next_resource),
                resource_node,
            )
    for indexable_attribute, value in next_resource.get_index_values_depending_on_indexes(
        indexable_attributes_added
    ).items():
        if indexable_attribute in indexable_attributes_added:
            # It was already added to the index in earlier steps
            continue
        elif value is None:
            # Index can only be calculated if all the required attributes are present
            # None value indicates one or more required attributes are not present
            # Therefore don't try to index this resource by this index type
            continue
        else:
            self._add_resource_attribute_to_index(indexable_attribute, value, resource_node)

    resource_node.model = next_resource
    return next_resource

rebase_resource(self, resource_id, new_parent_id) async

Move a resource which was a child to instead be a child of a different resource.

Parameters:

Name Type Description Default
resource_id bytes

resource ID of the resource to rebase

required
new_parent_id bytes

resource ID of the new parent resource for resource_id

required

Exceptions:

Type Description
NotFoundError

If there is not a resource with resource ID resource_id

NotFoundError

If there is not a resource with resource ID new_parent_id

Source code in ofrak/service/resource_service.py
async def rebase_resource(self, resource_id: bytes, new_parent_id: bytes):
    resource_node = self._resource_store.get(resource_id)
    if resource_node is None:
        raise NotFoundError(f"The resource {resource_id.hex()} does not exist")

    new_parent_resource_node = self._resource_store.get(new_parent_id)
    if new_parent_resource_node is None:
        raise NotFoundError(f"The new parent resource {resource_id.hex()} does not exist")

    former_parent_resource_node = resource_node.parent
    if former_parent_resource_node is not None:
        former_parent_resource_node.remove_child(resource_node)
    new_parent_resource_node.add_child(resource_node)
    resource_node.parent = new_parent_resource_node
    resource_node.model.parent_id = new_parent_id

delete_resource(self, resource_id) async

Delete a resource by ID and all of its children, removing them from the database. If no resource for the given ID is found, it is assumed the resource has already been deleted (does not raise an error).

Parameters:

Name Type Description Default
resource_id bytes

The ID of the resource to delete

required
Source code in ofrak/service/resource_service.py
async def delete_resource(self, resource_id: bytes):
    resource_node = self._resource_store.get(resource_id)
    if resource_node is None:
        # Already deleted, probably by an ancestor calling the recursive func below
        return

    former_parent_resource_node = resource_node.parent
    if former_parent_resource_node is not None:
        former_parent_resource_node.remove_child(resource_node)

    def _delete_resource_helper(_resource_node: ResourceNode):
        for child in _resource_node._children:
            _delete_resource_helper(child)

        for indexable_attribute, val in _resource_node.model.get_index_values().items():
            try:
                self._remove_resource_attribute_from_index(indexable_attribute, _resource_node)
            except ValueError as e:
                if val is None:
                    # Index value could not be calculated, so it is not surprising it was not
                    # in the index
                    continue
                else:
                    raise e

        tag_removal_blacklist: Set[ResourceTag] = set()
        for tag in _resource_node.model.tags:
            self._remove_resource_tag_from_index(tag, _resource_node, tag_removal_blacklist)
            tag_removal_blacklist.update(tag.tag_classes())

        del self._resource_store[_resource_node.model.id]
        if _resource_node.model.data_id is not None:
            del self._resource_by_data_id_store[_resource_node.model.data_id]

    _delete_resource_helper(resource_node)