diff --git a/tests/test_loaded.py b/tests/test_loaded.py index 92b8a10c0..0fe534e46 100644 --- a/tests/test_loaded.py +++ b/tests/test_loaded.py @@ -11,8 +11,9 @@ def test_remote(self): """ # get a unit cube from localhost with g.serve_meshes() as address: - mesh = g.trimesh.exchange.load.load_remote(url=address + "/unit_cube.STL") + scene = g.trimesh.exchange.load.load_remote(url=address + "/unit_cube.STL") + mesh = scene.to_mesh() assert g.np.isclose(mesh.volume, 1.0) assert isinstance(mesh, g.trimesh.Trimesh) diff --git a/trimesh/exchange/load.py b/trimesh/exchange/load.py index a2b9167ed..51282697e 100644 --- a/trimesh/exchange/load.py +++ b/trimesh/exchange/load.py @@ -156,11 +156,12 @@ def load_scene( Loaded geometry as trimesh classes """ - arg = _parse_file_args(file_obj=file_obj, file_type=file_type, resolver=resolver) - - if arg.is_remote: - if not allow_remote: - raise ValueError("URL passed with `allow_remote=False`") + arg = _parse_file_args( + file_obj=file_obj, + file_type=file_type, + resolver=resolver, + allow_remote=allow_remote, + ) try: if arg.file_type in path_formats(): @@ -326,7 +327,7 @@ def _load_compressed(file_obj, file_type=None, resolver=None, mixed=False, **kwa return result -def load_remote(url, **kwargs): +def load_remote(url, **kwargs) -> Scene: """ Load a mesh at a remote URL into a local trimesh object. @@ -345,34 +346,7 @@ def load_remote(url, **kwargs): loaded : Trimesh, Path, Scene Loaded result """ - # import here to keep requirement soft - import httpx - - # download the mesh - response = httpx.get(url, follow_redirects=True) - response.raise_for_status() - - # wrap as file object - file_obj = util.wrap_as_stream(response.content) - - # so loaders can access textures/etc - resolver = resolvers.WebResolver(url) - - try: - # if we have a bunch of query parameters the type - # will be wrong so try to clean up the URL - # urllib is Python 3 only - import urllib - - # remove the url-safe encoding then split off query params - file_type = urllib.parse.unquote(url).split("?", 1)[0].split("/")[-1].strip() - except BaseException: - # otherwise just use the last chunk of URL - file_type = url.split("/")[-1].split("?", 1)[0] - - # actually load the data from the retrieved bytes - loaded = load(file_obj=file_obj, file_type=file_type, resolver=resolver, **kwargs) - return loaded + return load_scene(file_obj=url, allow_remote=True, **kwargs) def _load_kwargs(*args, **kwargs) -> Geometry: @@ -516,14 +490,12 @@ class _FileArgs: # a resolver for loading assets next to the file resolver: resolvers.ResolverLike - # is this a remote url - is_remote: bool - def _parse_file_args( file_obj: Loadable, file_type: Optional[str], resolver: Optional[resolvers.ResolverLike] = None, + allow_remote: bool = False, **kwargs, ) -> _FileArgs: """ @@ -564,20 +536,12 @@ def _parse_file_args( Returns ----------- - file_obj : file-like object - Contains data - file_type : str - Lower case of the type of file (eg 'stl', 'dae', etc) - metadata : dict - Any metadata gathered - opened : bool - Did we open the file or not - resolver : trimesh.visual.Resolver - Resolver to load other assets + args + Populated `_FileArg` message """ + metadata = {} opened = False - is_remote = False if "metadata" in kwargs and isinstance(kwargs["metadata"], dict): metadata.update(kwargs["metadata"]) @@ -593,8 +557,7 @@ def _parse_file_args( try: # os.path.isfile will return False incorrectly # if we don't give it an absolute path - file_path = os.path.expanduser(file_obj) - file_path = os.path.abspath(file_path) + file_path = os.path.abspath(os.path.expanduser(file_obj)) exists = os.path.isfile(file_path) except BaseException: exists = False @@ -615,13 +578,23 @@ def _parse_file_args( opened = True else: if "{" in file_obj: - # if a dict bracket is in the string, its probably a straight - # JSON + # if a bracket is in the string it's probably straight JSON file_type = "json" elif "https://" in file_obj or "http://" in file_obj: - # we've been passed a URL, warn to use explicit function - # and don't do network calls via magical pipeline - raise ValueError(f"use load_remote to load URL: {file_obj}") + if not allow_remote: + raise ValueError("unable to load URL with `allow_remote=False`") + + import urllib + + # remove the url-safe encoding and query params + file_type = util.split_extension( + urllib.parse.unquote(file_obj).split("?", 1)[0].split("/")[-1].strip() + ) + # create a web resolver to do the fetching and whatnot + resolver = resolvers.WebResolver(url=file_obj) + # fetch the base file + file_obj = util.wrap_as_stream(resolver.get_base()) + elif file_type is None: raise ValueError(f"string is not a file: {file_obj}") @@ -656,7 +629,6 @@ def _parse_file_args( metadata=metadata, was_opened=opened, resolver=resolver, - is_remote=is_remote, ) diff --git a/trimesh/resolvers.py b/trimesh/resolvers.py index 4f0fcbeaf..b321b0461 100644 --- a/trimesh/resolvers.py +++ b/trimesh/resolvers.py @@ -335,6 +335,11 @@ def __init__(self, url: str): else: # recombine into string ignoring any double slashes path = "/".join(split) + + # save the URL we were created with, i.e. + # `https://stuff.com/models/thing.glb` + self.url = url + # save the root url, i.e. `https://stuff.com/models` self.base_url = ( "/".join( i @@ -382,6 +387,24 @@ def get(self, name: str) -> bytes: # return the bytes of the response return response.content + def get_base(self) -> bytes: + """ + Fetch the data at the full URL this resolver was + instantiated with, i.e. `https://stuff.com/hi.glb` + this will return the response. + + Returns + -------- + content + The value at `self.url` + """ + import httpx + + # just fetch the url we were created with + response = httpx.get(self.url, follow_redirects=True) + response.raise_for_status() + return response.content + def namespaced(self, namespace: str) -> "WebResolver": """ Return a namespaced version of current resolver.