diff --git a/pins/rsconnect/api.py b/pins/rsconnect/api.py index 880a6838..84efbd99 100644 --- a/pins/rsconnect/api.py +++ b/pins/rsconnect/api.py @@ -420,6 +420,15 @@ def misc_get_content_bundle_file( _download_file(r, f_obj) + def misc_get_content_bundle_file_info(self, guid: str, id: str, name: str): + + route = f"{self.server_url}/content/{guid}/_rev{id}/{name}" + + r = self._raw_query(route, return_request=True) + r.raise_for_status() + + return r.headers + def misc_get_applications( self, filter: str, count: int = 1000, search: str = None ) -> Paginated[Sequence[Content]]: diff --git a/pins/rsconnect/fs.py b/pins/rsconnect/fs.py index 077c95a9..1a69c1e0 100644 --- a/pins/rsconnect/fs.py +++ b/pins/rsconnect/fs.py @@ -1,3 +1,5 @@ +import logging + from dataclasses import dataclass, asdict, field, fields from pathlib import Path @@ -18,6 +20,10 @@ RSC_CODE_OBJECT_DOES_NOT_EXIST, ) + +_log = logging.getLogger(__name__) + + # Misc ---- @@ -131,6 +137,8 @@ def ls( "/" -> user content bundles """ + _log.debug("ls") + if isinstance(self.parse_path(path), EmptyPath): # root path specified, so list users all_results = self.api.get_users() @@ -183,6 +191,8 @@ def put( """ + _log.debug("put") + parsed = self.parse_path(rpath) if len(args) or len(kwargs): @@ -232,6 +242,8 @@ def put( def open(self, path: str, mode: str = "rb", *args, **kwargs): """Open a file inside an RStudio Connect bundle.""" + _log.debug("open") + if mode != "rb": raise NotImplementedError() @@ -260,6 +272,9 @@ def open(self, path: str, mode: str = "rb", *args, **kwargs): def get(self, rpath, lpath, recursive=False, *args, **kwargs) -> None: """Fetch a bundle or file from RStudio Connect.""" + + _log.debug("get") + parsed = self.parse_path(rpath) if recursive: @@ -278,6 +293,9 @@ def get(self, rpath, lpath, recursive=False, *args, **kwargs) -> None: ) def exists(self, path: str, **kwargs) -> bool: + + _log.debug("exists") + try: self.info(path) return True @@ -287,6 +305,9 @@ def exists(self, path: str, **kwargs) -> bool: def mkdir( self, path, create_parents=True, *args, access_type="acl", **kwargs ) -> None: + + _log.debug("mkdir") + parsed = self.parse_path(path) if len(args) or len(kwargs): @@ -302,12 +323,18 @@ def mkdir( self.api.post_content_item(parsed.content, access_type, **kwargs) def info(self, path, **kwargs) -> "User | Content | Bundle": + + _log.debug(f"info: {path}") + # TODO: source of fsspec info uses self._parent to check cache? # S3 encodes refresh (for local cache) and version_id arguments return self._get_entity_from_path(path) def rm(self, path, recursive=False, maxdepth=None) -> None: + + _log.debug("rm") + parsed = self.parse_path(path) # guards ---- @@ -354,7 +381,7 @@ def parse_path(self, path): raise ValueError(f"Unable to parse path: {path}") def _get_entity_from_path(self, path): - parsed = self.parse_path(path) + parsed = self.parse_path(self._strip_protocol(path)) # guard against empty paths if isinstance(parsed, EmptyPath): @@ -374,7 +401,12 @@ def _get_entity_from_path(self, path): if isinstance(parsed, BundlePath): content_guid = content["guid"] - crnt = self._get_content_bundle(content_guid, parsed.bundle) + crnt = bundle = self._get_content_bundle(content_guid, parsed.bundle) + + if isinstance(parsed, BundleFilePath): + crnt = self._get_content_bundle_file_info( + content["guid"], bundle["id"], parsed.file_name + ) return crnt @@ -413,6 +445,22 @@ def _get_content_bundle(self, content_guid, bundle_id): return bundle + def _get_content_bundle_file_info( + self, content_guid: str, bundle_id: str, fname: str + ): + headers = self.api.misc_get_content_bundle_file_info( + content_guid, bundle_id, fname + ) + + # convert result into something that looks like what fsspec's s3 + # implementation returns + return { + "name": fname, + "type": "file", + "size": headers["Content-Length"], + "ContentType": headers["Content-Type"], + } + def _get_user_from_name(self, name): """Fetch a single user entity from user name.""" users = self.api.get_users(prefix=name) diff --git a/pins/tests/test_rsconnect_api.py b/pins/tests/test_rsconnect_api.py index 0a9e559f..686a669d 100644 --- a/pins/tests/test_rsconnect_api.py +++ b/pins/tests/test_rsconnect_api.py @@ -363,6 +363,12 @@ def test_rsconnect_fs_info_root_ok(fs_short): assert susan == fs_short.info("susan") +def test_rsconnect_fs_strips_protocol(fs_short): + # the critical thing here is that we can include the protocol (rsc://) + info = fs_short.info("rsc://susan") + assert info["username"] == "susan" + + # fs.exists ----