6 Commits

Author SHA1 Message Date
Salad Dais
01c6931d53 v0.14.2 2023-12-24 18:05:05 +00:00
Salad Dais
493563bb6f Add a few asset type lookups 2023-12-24 06:47:04 +00:00
Salad Dais
ca5c71402b Bump Python requirement to 3.9 2023-12-24 05:57:14 +00:00
Salad Dais
ad765a1ede Load inventory cache in a background thread
llsd.parse_notation() is slow as hell, no way around it.
2023-12-24 05:55:56 +00:00
Salad Dais
9adee14e0f Allow non-byte legacy schema flag fields 2023-12-23 15:40:00 +00:00
Salad Dais
57c4bd0e7c Improve AIS support 2023-12-22 21:25:05 +00:00
8 changed files with 73 additions and 21 deletions

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.11"]
python-version: ["3.9", "3.11"]
steps:
- uses: actions/checkout@v2

View File

@@ -17,7 +17,6 @@ import inspect
import logging
import secrets
import struct
import typing
import weakref
from io import StringIO
from typing import *
@@ -49,6 +48,10 @@ class SchemaFlagField(SchemaHexInt):
"""Like a hex int, but must be serialized as bytes in LLSD due to being a U32"""
@classmethod
def from_llsd(cls, val: Any, flavor: str) -> int:
# Sometimes values in S32 range will just come through normally
if isinstance(val, int):
return val
if flavor == "legacy":
return struct.unpack("!I", val)[0]
return val
@@ -190,7 +193,7 @@ class InventoryBase(SchemaBase):
writer.write("\t}\n")
class InventoryDifferences(typing.NamedTuple):
class InventoryDifferences(NamedTuple):
changed: List[InventoryNodeBase]
removed: List[InventoryNodeBase]
@@ -400,7 +403,6 @@ class InventoryNodeBase(InventoryBase, _HasName):
@dataclasses.dataclass
class InventoryContainerBase(InventoryNodeBase):
# TODO: Not a string in AIS
type: AssetType = schema_field(SchemaEnumField(AssetType))
@property
@@ -461,7 +463,6 @@ class InventoryCategory(InventoryContainerBase):
VERSION_NONE: ClassVar[int] = -1
cat_id: UUID = schema_field(SchemaUUID)
# TODO: not a string in AIS
pref_type: FolderType = schema_field(SchemaEnumField(FolderType), llsd_name="preferred_type")
name: str = schema_field(SchemaMultilineStr)
owner_id: Optional[UUID] = schema_field(SchemaUUID, default=None)
@@ -488,6 +489,18 @@ class InventoryCategory(InventoryContainerBase):
type=AssetType.CATEGORY,
)
@classmethod
def _get_fields_dict(cls, llsd_flavor: Optional[str] = None):
fields = super()._get_fields_dict(llsd_flavor)
if llsd_flavor == "ais":
# AIS is smart enough to know that all categories are asset type category...
fields.pop("type")
# These have different names though
fields["type_default"] = fields.pop("preferred_type")
fields["agent_id"] = fields.pop("owner_id")
fields["category_id"] = fields.pop("cat_id")
return fields
__hash__ = InventoryNodeBase.__hash__
@@ -573,5 +586,12 @@ class InventoryItem(InventoryNodeBase):
creation_date=block["CreationDate"],
)
def to_llsd(self, flavor: str = "legacy"):
val = super().to_llsd(flavor=flavor)
if flavor == "ais":
# There's little chance this differs from owner ID, just place it.
val["agent_id"] = val["permissions"]["owner_id"]
return val
INVENTORY_TYPES: Tuple[Type[InventoryNodeBase], ...] = (InventoryCategory, InventoryObject, InventoryItem)

View File

@@ -104,6 +104,13 @@ class SchemaStr(SchemaFieldSerializer[str]):
class SchemaUUID(SchemaFieldSerializer[UUID]):
@classmethod
def from_llsd(cls, val: Any, flavor: str) -> UUID:
# FetchInventory2 will return a string, but we want a UUID. It's not an issue
# for us to return a UUID later there because it'll just cast to string if
# that's what it wants
return UUID(val)
@classmethod
def deserialize(cls, val: str) -> UUID:
return UUID(val)
@@ -157,11 +164,11 @@ def parse_schema_line(line: str):
@dataclasses.dataclass
class SchemaBase(abc.ABC):
@classmethod
def _get_fields_dict(cls, llsd=False):
def _get_fields_dict(cls, llsd_flavor: Optional[str] = None):
fields_dict = {}
for field in dataclasses.fields(cls):
field_name = field.name
if llsd:
if llsd_flavor:
field_name = field.metadata.get("llsd_name") or field_name
fields_dict[field_name] = field
return fields_dict
@@ -181,7 +188,7 @@ class SchemaBase(abc.ABC):
@classmethod
def from_llsd(cls, inv_dict: Dict, flavor: str = "legacy"):
fields = cls._get_fields_dict(llsd=True)
fields = cls._get_fields_dict(llsd_flavor=flavor)
obj_dict = {}
for key, val in inv_dict.items():
if key in fields:
@@ -205,7 +212,10 @@ class SchemaBase(abc.ABC):
else:
raise ValueError(f"Unsupported spec for {key!r}, {spec!r}")
else:
LOG.warning(f"Unknown key {key!r}")
if flavor != "ais":
# AIS has a number of different fields that are irrelevant depending on
# what exactly sent the payload
LOG.warning(f"Unknown key {key!r}")
return cls._obj_from_dict(obj_dict)
def to_bytes(self) -> bytes:
@@ -219,7 +229,7 @@ class SchemaBase(abc.ABC):
def to_llsd(self, flavor: str = "legacy"):
obj_dict = {}
for field_name, field in self._get_fields_dict(llsd=True).items():
for field_name, field in self._get_fields_dict(llsd_flavor=flavor).items():
spec = field.metadata.get("spec")
# Not meant to be serialized
if not spec:

View File

@@ -37,13 +37,15 @@ class LookupIntEnum(IntEnum):
_ASSET_TYPE_BIDI: BiDiDict[str] = BiDiDict({
"animation": "animatn",
"callingcard": "callcard",
"texture_tga": "txtr_tga",
"image_tga": "img_tga",
"sound_wav": "snd_wav",
"lsl_text": "lsltext",
"lsl_bytecode": "lslbyte",
"texture_tga": "txtr_tga",
"image_tga": "img_tga",
"image_jpeg": "jpg",
"sound_wav": "snd_wav",
"folder_link": "link_f",
"unknown": "invalid",
"none": "-1",
})

View File

@@ -86,6 +86,7 @@ class InventoryManager:
self.model.add(cached_item)
def _parse_cache(self, path: Union[str, Path]) -> Tuple[List[InventoryCategory], List[InventoryItem]]:
"""Warning, may be incredibly slow due to llsd.parse_notation() behavior"""
categories: List[InventoryCategory] = []
items: List[InventoryItem] = []
# Parse our cached items and categories out of the compressed inventory cache

View File

@@ -1,5 +1,5 @@
import asyncio
import datetime as dt
import logging
from hippolyzer.lib.base.helpers import get_mtime
from hippolyzer.lib.client.inventory_manager import InventoryManager
@@ -12,6 +12,8 @@ class ProxyInventoryManager(InventoryManager):
super().__init__(session)
newest_cache = None
newest_timestamp = dt.datetime(year=1970, month=1, day=1, tzinfo=dt.timezone.utc)
# So consumers know when the inventory should be complete
self.cache_loaded: asyncio.Event = asyncio.Event()
# Look for the newest version of the cached inventory and use that.
# Not foolproof, but close enough if we're not sure what viewer is being used.
for cache_dir in iter_viewer_cache_dirs():
@@ -26,7 +28,8 @@ class ProxyInventoryManager(InventoryManager):
newest_cache = inv_cache_path
if newest_cache:
try:
self.load_cache(newest_cache)
except:
logging.exception("Failed to load invcache")
cache_load_fut = asyncio.ensure_future(asyncio.to_thread(self.load_cache, newest_cache))
# Meh. Don't care if it fails.
cache_load_fut.add_done_callback(lambda *args: self.cache_loaded.set())
else:
self.cache_loaded.set()

View File

@@ -25,7 +25,7 @@ from setuptools import setup, find_packages
here = path.abspath(path.dirname(__file__))
version = '0.14.1'
version = '0.14.2'
with open(path.join(here, 'README.md')) as readme_fh:
readme = readme_fh.read()
@@ -42,7 +42,6 @@ setup(
"Operating System :: POSIX",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@@ -80,7 +79,7 @@ setup(
}
},
zip_safe=False,
python_requires='>=3.8',
python_requires='>=3.9',
install_requires=[
'llsd<1.1.0',
'defusedxml',

View File

@@ -54,6 +54,7 @@ INV_CATEGORY = """\tinv_category\t0
\t\ttype\tlsltext
\t\tpref_type\tlsltext
\t\tname\tScripts|
\t\towner_id\ta2e76fcd-9360-4f6d-a924-000000000003
\t}
"""
@@ -160,6 +161,22 @@ class TestLegacyInv(unittest.TestCase):
]
)
def test_llsd_serialization_ais(self):
model = InventoryModel.from_str(INV_CATEGORY)
self.assertEqual(
[
{
'agent_id': UUID('a2e76fcd-9360-4f6d-a924-000000000003'),
'category_id': UUID('f4d91477-def1-487a-b4f3-6fa201c17376'),
'name': 'Scripts',
'parent_id': UUID('00000000-0000-0000-0000-000000000000'),
'type_default': 10,
'version': -1
}
],
model.to_llsd("ais")
)
def test_llsd_legacy_equality(self):
new_model = InventoryModel.from_llsd(self.model.to_llsd())
self.assertEqual(self.model, new_model)