Add InventoryModel diffing
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user