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)
|
file_name=inv_message["InventoryData"]["Filename"], file_path=XferFilePath.CACHE)
|
||||||
inv_model = InventoryModel.from_bytes(xfer.reassemble_chunks())
|
inv_model = InventoryModel.from_bytes(xfer.reassemble_chunks())
|
||||||
first_script: Optional[InventoryItem] = None
|
first_script: Optional[InventoryItem] = None
|
||||||
for item in inv_model.items.values():
|
for item in inv_model.all_items:
|
||||||
if item.type == "lsltext":
|
if item.type == "lsltext":
|
||||||
first_script = item
|
first_script = item
|
||||||
if not first_script:
|
if not first_script:
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class XferExampleAddon(BaseAddon):
|
|||||||
await xfer
|
await xfer
|
||||||
|
|
||||||
inv_model = InventoryModel.from_bytes(xfer.reassemble_chunks())
|
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)
|
show_message(item_names)
|
||||||
|
|
||||||
@handle_command()
|
@handle_command()
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import itertools
|
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
|
import typing
|
||||||
import weakref
|
import weakref
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from typing import *
|
from typing import *
|
||||||
@@ -132,17 +132,16 @@ class InventoryBase(SchemaBase):
|
|||||||
writer.write("\t}\n")
|
writer.write("\t}\n")
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryDifferences(typing.NamedTuple):
|
||||||
|
changed: List[InventoryNodeBase]
|
||||||
|
removed: List[InventoryNodeBase]
|
||||||
|
|
||||||
|
|
||||||
class InventoryModel(InventoryBase):
|
class InventoryModel(InventoryBase):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.containers: Dict[UUID, InventoryContainerBase] = {}
|
self.nodes: Dict[UUID, InventoryNodeBase] = {}
|
||||||
self.items: Dict[UUID, InventoryItem] = {}
|
|
||||||
self.root: Optional[InventoryContainerBase] = None
|
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
|
@classmethod
|
||||||
def from_reader(cls, reader: StringIO, read_header=False) -> InventoryModel:
|
def from_reader(cls, reader: StringIO, read_header=False) -> InventoryModel:
|
||||||
model = cls()
|
model = cls()
|
||||||
@@ -180,32 +179,43 @@ class InventoryModel(InventoryBase):
|
|||||||
LOG.warning(f"Unknown object type {obj_dict!r}")
|
LOG.warning(f"Unknown object type {obj_dict!r}")
|
||||||
return model
|
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):
|
def to_writer(self, writer: StringIO):
|
||||||
for container in self.containers.values():
|
for node in self.ordered_nodes:
|
||||||
container.to_writer(writer)
|
node.to_writer(writer)
|
||||||
for item in self.items.values():
|
|
||||||
item.to_writer(writer)
|
|
||||||
|
|
||||||
def to_llsd(self):
|
def to_llsd(self):
|
||||||
vals = []
|
return list(node.to_llsd() for node in self.ordered_nodes)
|
||||||
for container in self.containers.values():
|
|
||||||
vals.append(container.to_llsd())
|
|
||||||
for item in self.items.values():
|
|
||||||
vals.append(item.to_llsd())
|
|
||||||
return vals
|
|
||||||
|
|
||||||
def add(self, node: InventoryNodeBase):
|
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")
|
raise KeyError(f"{node.node_id} already exists in the inventory model")
|
||||||
|
|
||||||
|
self.nodes[node.node_id] = node
|
||||||
if isinstance(node, InventoryContainerBase):
|
if isinstance(node, InventoryContainerBase):
|
||||||
self.containers[node.node_id] = node
|
|
||||||
if node.parent_id == UUID.ZERO:
|
if node.parent_id == UUID.ZERO:
|
||||||
self.root = node
|
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)
|
node.model = weakref.proxy(self)
|
||||||
|
|
||||||
def unlink(self, node: InventoryNodeBase) -> Sequence[InventoryNodeBase]:
|
def unlink(self, node: InventoryNodeBase) -> Sequence[InventoryNodeBase]:
|
||||||
@@ -217,17 +227,35 @@ class InventoryModel(InventoryBase):
|
|||||||
if isinstance(node, InventoryContainerBase):
|
if isinstance(node, InventoryContainerBase):
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
unlinked.extend(self.unlink(child))
|
unlinked.extend(self.unlink(child))
|
||||||
self.items.pop(node.node_id, None)
|
self.nodes.pop(node.node_id, None)
|
||||||
self.containers.pop(node.node_id, None)
|
|
||||||
node.model = None
|
node.model = None
|
||||||
return unlinked
|
return unlinked
|
||||||
|
|
||||||
@property
|
def get_differences(self, other: InventoryModel) -> InventoryDifferences:
|
||||||
def all_nodes(self) -> Iterable[InventoryNodeBase]:
|
# Includes modified things with the same ID
|
||||||
for container in self.containers.values():
|
changed_in_other = []
|
||||||
yield container
|
removed_in_other = []
|
||||||
for item in self.items.values():
|
|
||||||
yield item
|
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
|
@dataclasses.dataclass
|
||||||
@@ -266,9 +294,13 @@ class InventoryNodeBase(InventoryBase):
|
|||||||
def node_id(self) -> UUID:
|
def node_id(self) -> UUID:
|
||||||
return getattr(self, self.ID_ATTR)
|
return getattr(self, self.ID_ATTR)
|
||||||
|
|
||||||
|
@node_id.setter
|
||||||
|
def node_id(self, val: UUID):
|
||||||
|
setattr(self, self.ID_ATTR, val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self) -> Optional[InventoryContainerBase]:
|
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]:
|
def unlink(self) -> Sequence[InventoryNodeBase]:
|
||||||
return self.model.unlink(self)
|
return self.model.unlink(self)
|
||||||
@@ -282,6 +314,9 @@ class InventoryNodeBase(InventoryBase):
|
|||||||
return None
|
return None
|
||||||
return super()._obj_from_dict(obj_dict)
|
return super()._obj_from_dict(obj_dict)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.node_id)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class InventoryContainerBase(InventoryNodeBase):
|
class InventoryContainerBase(InventoryNodeBase):
|
||||||
@@ -291,12 +326,13 @@ class InventoryContainerBase(InventoryNodeBase):
|
|||||||
@property
|
@property
|
||||||
def children(self) -> Sequence[InventoryNodeBase]:
|
def children(self) -> Sequence[InventoryNodeBase]:
|
||||||
return tuple(
|
return tuple(
|
||||||
x for x in (
|
x for x in self.model.nodes.values()
|
||||||
itertools.chain(self.model.containers.values(), self.model.items.values())
|
|
||||||
)
|
|
||||||
if x.parent_id == self.node_id
|
if x.parent_id == self.node_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# So autogenerated __hash__ doesn't kill our inherited one
|
||||||
|
__hash__ = InventoryNodeBase.__hash__
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class InventoryObject(InventoryContainerBase):
|
class InventoryObject(InventoryContainerBase):
|
||||||
@@ -305,6 +341,8 @@ class InventoryObject(InventoryContainerBase):
|
|||||||
|
|
||||||
obj_id: UUID = schema_field(SchemaUUID)
|
obj_id: UUID = schema_field(SchemaUUID)
|
||||||
|
|
||||||
|
__hash__ = InventoryNodeBase.__hash__
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class InventoryCategory(InventoryContainerBase):
|
class InventoryCategory(InventoryContainerBase):
|
||||||
@@ -316,6 +354,8 @@ class InventoryCategory(InventoryContainerBase):
|
|||||||
owner_id: UUID = schema_field(SchemaUUID)
|
owner_id: UUID = schema_field(SchemaUUID)
|
||||||
version: int = schema_field(SchemaInt)
|
version: int = schema_field(SchemaInt)
|
||||||
|
|
||||||
|
__hash__ = InventoryNodeBase.__hash__
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class InventoryItem(InventoryNodeBase):
|
class InventoryItem(InventoryNodeBase):
|
||||||
@@ -334,6 +374,8 @@ class InventoryItem(InventoryNodeBase):
|
|||||||
asset_id: Optional[UUID] = schema_field(SchemaUUID, default=None)
|
asset_id: Optional[UUID] = schema_field(SchemaUUID, default=None)
|
||||||
shadow_id: Optional[UUID] = schema_field(SchemaUUID, default=None)
|
shadow_id: Optional[UUID] = schema_field(SchemaUUID, default=None)
|
||||||
|
|
||||||
|
__hash__ = InventoryNodeBase.__hash__
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def true_asset_id(self) -> UUID:
|
def true_asset_id(self) -> UUID:
|
||||||
if self.asset_id is not None:
|
if self.asset_id is not None:
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class TestLegacyInv(unittest.TestCase):
|
|||||||
self.model = InventoryModel.from_str(SIMPLE_INV)
|
self.model = InventoryModel.from_str(SIMPLE_INV)
|
||||||
|
|
||||||
def test_parse(self):
|
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)
|
self.assertIsNotNone(self.model.root)
|
||||||
|
|
||||||
def test_serialize(self):
|
def test_serialize(self):
|
||||||
@@ -58,39 +58,38 @@ class TestLegacyInv(unittest.TestCase):
|
|||||||
self.assertEqual(self.model, new_model)
|
self.assertEqual(self.model, new_model)
|
||||||
|
|
||||||
def test_item_access(self):
|
def test_item_access(self):
|
||||||
|
item = self.model.nodes[UUID('dd163122-946b-44df-99f6-a6030e2b9597')]
|
||||||
item = self.model.items[UUID('dd163122-946b-44df-99f6-a6030e2b9597')]
|
|
||||||
self.assertEqual(item.name, "New Script")
|
self.assertEqual(item.name, "New Script")
|
||||||
self.assertEqual(item.sale_info.sale_type, "not")
|
self.assertEqual(item.sale_info.sale_type, "not")
|
||||||
self.assertEqual(item.model, self.model)
|
self.assertEqual(item.model, self.model)
|
||||||
|
|
||||||
def test_access_children(self):
|
def test_access_children(self):
|
||||||
root = self.model.root
|
root = self.model.root
|
||||||
item = tuple(self.model.items.values())[0]
|
item = tuple(self.model.ordered_nodes)[1]
|
||||||
self.assertEqual((item,), root.children)
|
self.assertEqual((item,), root.children)
|
||||||
|
|
||||||
def test_access_parent(self):
|
def test_access_parent(self):
|
||||||
root = self.model.root
|
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(root, item.parent)
|
||||||
self.assertEqual(None, root.parent)
|
self.assertEqual(None, root.parent)
|
||||||
|
|
||||||
def test_unlink(self):
|
def test_unlink(self):
|
||||||
self.assertEqual(1, len(self.model.root.children))
|
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([item], item.unlink())
|
||||||
self.assertEqual(0, len(self.model.root.children))
|
self.assertEqual(0, len(self.model.root.children))
|
||||||
self.assertEqual(None, item.model)
|
self.assertEqual(None, item.model)
|
||||||
|
|
||||||
def test_relink(self):
|
def test_relink(self):
|
||||||
item = tuple(self.model.items.values())[0]
|
item = tuple(self.model.ordered_nodes)[1]
|
||||||
for unlinked in item.unlink():
|
for unlinked in item.unlink():
|
||||||
self.model.add(unlinked)
|
self.model.add(unlinked)
|
||||||
self.assertEqual(self.model, item.model)
|
self.assertEqual(self.model, item.model)
|
||||||
self.assertEqual(1, len(self.model.root.children))
|
self.assertEqual(1, len(self.model.root.children))
|
||||||
|
|
||||||
def test_eq_excludes_model(self):
|
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 = copy.copy(item)
|
||||||
item_copy.model = None
|
item_copy.model = None
|
||||||
self.assertEqual(item, item_copy)
|
self.assertEqual(item, item_copy)
|
||||||
@@ -137,9 +136,33 @@ class TestLegacyInv(unittest.TestCase):
|
|||||||
def test_llsd_legacy_equality(self):
|
def test_llsd_legacy_equality(self):
|
||||||
new_model = InventoryModel.from_llsd(self.model.to_llsd())
|
new_model = InventoryModel.from_llsd(self.model.to_llsd())
|
||||||
self.assertEqual(self.model, new_model)
|
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)
|
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_SHAPE = """LLWearable version 22
|
||||||
Girl Next Door - C2 - med - Adam n Eve
|
Girl Next Door - C2 - med - Adam n Eve
|
||||||
|
|||||||
Reference in New Issue
Block a user