Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pins/rsconnect/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]:
Expand Down
52 changes: 50 additions & 2 deletions pins/rsconnect/fs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import logging

from dataclasses import dataclass, asdict, field, fields
from pathlib import Path

Expand All @@ -18,6 +20,10 @@
RSC_CODE_OBJECT_DOES_NOT_EXIST,
)


_log = logging.getLogger(__name__)


# Misc ----


Expand Down Expand Up @@ -131,6 +137,8 @@ def ls(
"<username>/<content>" -> 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()
Expand Down Expand Up @@ -183,6 +191,8 @@ def put(

"""

_log.debug("put")

parsed = self.parse_path(rpath)

if len(args) or len(kwargs):
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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):
Expand All @@ -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 ----
Expand Down Expand Up @@ -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):
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions pins/tests/test_rsconnect_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----


Expand Down