Add InventoryModel diffing

This commit is contained in:
Salad Dais
2022-07-09 02:48:23 +00:00
parent f3c8015366
commit 289073be8e
4 changed files with 112 additions and 47 deletions

View File

@@ -47,7 +47,7 @@ class TransferExampleAddon(BaseAddon):
file_name=inv_message["InventoryData"]["Filename"], file_path=XferFilePath.CACHE)
inv_model = InventoryModel.from_bytes(xfer.reassemble_chunks())
first_script: Optional[InventoryItem] = None
for item in inv_model.items.values():
for item in inv_model.all_items:
if item.type == "lsltext":
first_script = item
if not first_script:

View File

@@ -57,7 +57,7 @@ class XferExampleAddon(BaseAddon):
await xfer
inv_model = InventoryModel.from_bytes(xfer.reassemble_chunks())
item_names = [item.name for item in inv_model.items.values()]
item_names = [item.name for item in inv_model.all_items]
show_message(item_names)
@handle_command()

View File

@@ -7,9 +7,9 @@ from __future__ import annotations
import dataclasses
import datetime as dt
import itertools
import logging
import struct
import typing
import weakref
from io import StringIO
from typing import *
@@ -132,17 +132,16 @@ class InventoryBase(SchemaBase):
writer.write("\t}\n")
class InventoryDifferences(typing.NamedTuple):
changed: List[InventoryNodeBase]
removed: List[InventoryNodeBase]
class InventoryModel(InventoryBase):
def __init__(self):
self.containers: Dict[UUID, InventoryContainerBase] = {}
self.items: Dict[UUID, InventoryItem] = {}
self.nodes: Dict[UUID, InventoryNodeBase] = {}
self.root: Optional[InventoryContainerBase] = None
def __eq__(self, other):
if not isinstance(other, InventoryModel):
return False
return tuple(self.all_nodes) == tuple(other.all_nodes)
@classmethod
def from_reader(cls, reader: StringIO, read_header=False) -> InventoryModel:
model = cls()
@@ -180,32 +179,43 @@ class InventoryModel(InventoryBase):
LOG.warning(f"Unknown object type {obj_dict!r}")
return model
@property
def ordered_nodes(self) -> Iterable[InventoryNodeBase]:
yield from self.all_containers
yield from self.all_items
@property
def all_containers(self) -> Iterable[InventoryContainerBase]:
for node in self.nodes.values():
if isinstance(node, InventoryContainerBase):
yield node
@property
def all_items(self) -> Iterable[InventoryItem]:
for node in self.nodes.values():
if not isinstance(node, InventoryContainerBase):
yield node
def __eq__(self, other):
if not isinstance(other, InventoryModel):
return False
return set(self.nodes.values()) == set(other.nodes.values())
def to_writer(self, writer: StringIO):
for container in self.containers.values():
container.to_writer(writer)
for item in self.items.values():
item.to_writer(writer)
for node in self.ordered_nodes:
node.to_writer(writer)
def to_llsd(self):
vals = []
for container in self.containers.values():
vals.append(container.to_llsd())
for item in self.items.values():
vals.append(item.to_llsd())
return vals
return list(node.to_llsd() for node in self.ordered_nodes)
def add(self, node: InventoryNodeBase):
if node.node_id in self.items or node.node_id in self.containers:
if node.node_id in self.nodes:
raise KeyError(f"{node.node_id} already exists in the inventory model")
self.nodes[node.node_id] = node
if isinstance(node, InventoryContainerBase):
self.containers[node.node_id] = node
if node.parent_id == UUID.ZERO:
self.root = node
elif isinstance(node, InventoryItem):
self.items[node.node_id] = node
else:
raise ValueError(f"Unknown node type for {node!r}")
node.model = weakref.proxy(self)
def unlink(self, node: InventoryNodeBase) -> Sequence[InventoryNodeBase]:
@@ -217,17 +227,35 @@ class InventoryModel(InventoryBase):
if isinstance(node, InventoryContainerBase):
for child in node.children:
unlinked.extend(self.unlink(child))
self.items.pop(node.node_id, None)
self.containers.pop(node.node_id, None)
self.nodes.pop(node.node_id, None)
node.model = None
return unlinked
@property
def all_nodes(self) -> Iterable[InventoryNodeBase]:
for container in self.containers.values():
yield container
for item in self.items.values():
yield item
def get_differences(self, other: InventoryModel) -> InventoryDifferences:
# Includes modified things with the same ID
changed_in_other = []
removed_in_other = []
other_keys = set(other.nodes.keys())
our_keys = set(self.nodes.keys())
# Removed
for key in our_keys - other_keys:
removed_in_other.append(self.nodes[key])
# Updated
for key in other_keys.intersection(our_keys):
other_node = other.nodes[key]
if other_node != self.nodes[key]:
changed_in_other.append(other_node)
# Added
for key in other_keys - our_keys:
changed_in_other.append(other.nodes[key])
return InventoryDifferences(
changed=changed_in_other,
removed=removed_in_other,
)
@dataclasses.dataclass
@@ -266,9 +294,13 @@ class InventoryNodeBase(InventoryBase):
def node_id(self) -> UUID:
return getattr(self, self.ID_ATTR)
@node_id.setter
def node_id(self, val: UUID):
setattr(self, self.ID_ATTR, val)
@property
def parent(self) -> Optional[InventoryContainerBase]:
return self.model.containers.get(self.parent_id)
return self.model.nodes.get(self.parent_id)
def unlink(self) -> Sequence[InventoryNodeBase]:
return self.model.unlink(self)
@@ -282,6 +314,9 @@ class InventoryNodeBase(InventoryBase):
return None
return super()._obj_from_dict(obj_dict)
def __hash__(self):
return hash(self.node_id)
@dataclasses.dataclass
class InventoryContainerBase(InventoryNodeBase):
@@ -291,12 +326,13 @@ class InventoryContainerBase(InventoryNodeBase):
@property
def children(self) -> Sequence[InventoryNodeBase]:
return tuple(
x for x in (
itertools.chain(self.model.containers.values(), self.model.items.values())
)
x for x in self.model.nodes.values()
if x.parent_id == self.node_id
)
# So autogenerated __hash__ doesn't kill our inherited one
__hash__ = InventoryNodeBase.__hash__
@dataclasses.dataclass
class InventoryObject(InventoryContainerBase):
@@ -305,6 +341,8 @@ class InventoryObject(InventoryContainerBase):
obj_id: UUID = schema_field(SchemaUUID)
__hash__ = InventoryNodeBase.__hash__
@dataclasses.dataclass
class InventoryCategory(InventoryContainerBase):
@@ -316,6 +354,8 @@ class InventoryCategory(InventoryContainerBase):
owner_id: UUID = schema_field(SchemaUUID)
version: int = schema_field(SchemaInt)
__hash__ = InventoryNodeBase.__hash__
@dataclasses.dataclass
class InventoryItem(InventoryNodeBase):
@@ -334,6 +374,8 @@ class InventoryItem(InventoryNodeBase):
asset_id: Optional[UUID] = schema_field(SchemaUUID, default=None)
shadow_id: Optional[UUID] = schema_field(SchemaUUID, default=None)
__hash__ = InventoryNodeBase.__hash__
@property
def true_asset_id(self) -> UUID:
if self.asset_id is not None:

View File

@@ -49,7 +49,7 @@ class TestLegacyInv(unittest.TestCase):
self.model = InventoryModel.from_str(SIMPLE_INV)
def test_parse(self):
self.assertTrue(UUID('f4d91477-def1-487a-b4f3-6fa201c17376') in self.model.containers)
self.assertTrue(UUID('f4d91477-def1-487a-b4f3-6fa201c17376') in self.model.nodes)
self.assertIsNotNone(self.model.root)
def test_serialize(self):
@@ -58,39 +58,38 @@ class TestLegacyInv(unittest.TestCase):
self.assertEqual(self.model, new_model)
def test_item_access(self):
item = self.model.items[UUID('dd163122-946b-44df-99f6-a6030e2b9597')]
item = self.model.nodes[UUID('dd163122-946b-44df-99f6-a6030e2b9597')]
self.assertEqual(item.name, "New Script")
self.assertEqual(item.sale_info.sale_type, "not")
self.assertEqual(item.model, self.model)
def test_access_children(self):
root = self.model.root
item = tuple(self.model.items.values())[0]
item = tuple(self.model.ordered_nodes)[1]
self.assertEqual((item,), root.children)
def test_access_parent(self):
root = self.model.root
item = tuple(self.model.items.values())[0]
item = tuple(self.model.ordered_nodes)[1]
self.assertEqual(root, item.parent)
self.assertEqual(None, root.parent)
def test_unlink(self):
self.assertEqual(1, len(self.model.root.children))
item = tuple(self.model.items.values())[0]
item = tuple(self.model.ordered_nodes)[1]
self.assertEqual([item], item.unlink())
self.assertEqual(0, len(self.model.root.children))
self.assertEqual(None, item.model)
def test_relink(self):
item = tuple(self.model.items.values())[0]
item = tuple(self.model.ordered_nodes)[1]
for unlinked in item.unlink():
self.model.add(unlinked)
self.assertEqual(self.model, item.model)
self.assertEqual(1, len(self.model.root.children))
def test_eq_excludes_model(self):
item = tuple(self.model.items.values())[0]
item = tuple(self.model.ordered_nodes)[1]
item_copy = copy.copy(item)
item_copy.model = None
self.assertEqual(item, item_copy)
@@ -137,9 +136,33 @@ class TestLegacyInv(unittest.TestCase):
def test_llsd_legacy_equality(self):
new_model = InventoryModel.from_llsd(self.model.to_llsd())
self.assertEqual(self.model, new_model)
tuple(new_model.containers.values())[0].name = "foo"
new_model.root.name = "foo"
self.assertNotEqual(self.model, new_model)
def test_difference_added(self):
new_model = InventoryModel.from_llsd(self.model.to_llsd())
diff = self.model.get_differences(new_model)
self.assertEqual([], diff.changed)
self.assertEqual([], diff.removed)
new_model.root.name = "foo"
diff = self.model.get_differences(new_model)
self.assertEqual([new_model.root], diff.changed)
self.assertEqual([], diff.removed)
item = new_model.root.children[0]
item.unlink()
diff = self.model.get_differences(new_model)
self.assertEqual([new_model.root], diff.changed)
self.assertEqual([item], diff.removed)
new_item = copy.copy(item)
new_item.node_id = UUID.random()
new_model.add(new_item)
diff = self.model.get_differences(new_model)
self.assertEqual([new_model.root, new_item], diff.changed)
self.assertEqual([item], diff.removed)
GIRL_NEXT_DOOR_SHAPE = """LLWearable version 22
Girl Next Door - C2 - med - Adam n Eve