Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01c6931d53 | ||
|
|
493563bb6f | ||
|
|
ca5c71402b | ||
|
|
ad765a1ede | ||
|
|
9adee14e0f | ||
|
|
57c4bd0e7c |
2
.github/workflows/pytest.yml
vendored
2
.github/workflows/pytest.yml
vendored
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
5
setup.py
5
setup.py
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user