Skip to content

Commit 2cec93a

Browse files
authored
Merge pull request #147 from rstudio/feat-rsc-public
feat!: support public rsc pin read, using manual board
2 parents 2e622f9 + 8e9d8b6 commit 2cec93a

File tree

7 files changed

+131
-22
lines changed

7 files changed

+131
-22
lines changed

docs/api/constructors.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ Board Constructors
1313
~board_gcs
1414
~board_azure
1515
~board_rsconnect
16+
~board_url
1617
~board

docs/api/index.rst

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,18 @@ Boards abstract over different storage backends, making it easy to share data in
3232
.. list-table::
3333
:class: table-align-left
3434

35-
* - :func:`.board_folder`, :func:`.board_local`
35+
* - :func:`.board_azure`
36+
- Use an Azure storage container as a board
37+
* - :func:`.board_folder`, :func:`.board_local`, :func:`.board_temp`
3638
- Use a local folder as a board
3739
* - :func:`.board_rsconnect`
3840
- Use RStudio Connect as a board
3941
* - :func:`.board_s3`
4042
- Use an S3 bucket as a board
4143
* - :func:`.board_gcs`
42-
- Use an Google Cloud Storage bucket as a board
43-
* - :func:`.board_azure`
44-
- Use an Azure Datalake storage container as a board.
44+
- Use a Google Cloud Storage bucket as a board.
45+
* - :func:`.board_url`
46+
- Use a dictionary of URLs as a board
4547
* - :func:`.board`
4648
- Generic board constructor
4749

pins/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
board_temp,
1717
board_local,
1818
board_github,
19-
board_urls,
19+
board_urls, # DEPRECATED
20+
board_url,
2021
board_rsconnect,
2122
board_azure,
2223
board_s3,

pins/boards.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ class BoardManual(BaseBoard):
637637
>>> import fsspec
638638
>>> import os
639639
>>> fs = fsspec.filesystem("github", org = "machow", repo = "pins-python")
640-
>>> pin_paths = {"df_csv": "df_csv/20220214T163720Z-9bfad"}
640+
>>> pin_paths = {"df_csv": "df_csv/20220214T163720Z-9bfad/"}
641641
>>> board = BoardManual("pins/tests/pins-compat", fs, pin_paths=pin_paths)
642642
643643
>>> board.pin_list()
@@ -690,7 +690,9 @@ def pin_meta(self, name, version=None):
690690
# create metadata, rather than read from a file
691691
return self.meta_factory.create_raw(path_to_pin, type="file", name=pin_name)
692692

693-
path_meta = self.construct_path([pin_name, meta_name])
693+
# note that pins on this board should point to versions, so we use an
694+
# empty string to mark version (it ultimately is ignored)
695+
path_meta = self.construct_path([pin_name, "", meta_name])
694696
f = self._open_pin_meta(path_meta)
695697
meta = self.meta_factory.read_pin_yaml(f, pin_name, VersionRaw(""))
696698

@@ -717,21 +719,34 @@ def construct_path(self, elements):
717719
pin_name, *others = elements
718720
pin_path = self.pin_paths[pin_name]
719721

720-
if self.board.strip() == "":
721-
return pin_path
722+
pre_components = [] if not self.board else [self.board]
722723

723-
if len(others):
724-
# this is confusing, but R pins url board has a final "/" indicate that
725-
# something is a pin version, rather than a single file. but since other
726-
# boards forbid a final /, we need to strip it off to join elements
727-
pin_path = pin_path.rstrip().rstrip("/")
724+
# note that for paths where version is specified, it gets omitted,
725+
# since pin_path should point to a pin version
726+
if not pin_path.endswith("/"):
727+
if len(others):
728+
raise ValueError(
729+
f"pin path {pin_path} does not end in '/' so is assumed to be a"
730+
f" single file. Cannot construct a path to elements {elements}."
731+
)
732+
return "/".join(pre_components + [pin_path])
733+
734+
# handle paths to pins (i.e. end with /) ----
735+
stripped = pin_path[:-1]
736+
737+
if len(others) == 0:
738+
return "/".join(pre_components + [pin_path])
739+
elif len(others) == 1:
740+
version = others[0]
741+
return "/".join(pre_components + [pin_path])
742+
elif len(others) == 2:
743+
version, meta = others
728744

729-
# this is a bit hacky, but this board only aims at specific pins versions,
730-
# so the metadata has the version as "", so we need to remove it.
731-
if others[0] == "":
732-
return super().construct_path([pin_path, *others[1:]])
745+
return "/".join(pre_components + [stripped, meta])
733746

734-
return super().construct_path([pin_path, *others])
747+
raise NotImplementedError(
748+
f"Unable to construct path from these elements: {elements}"
749+
)
735750

736751

737752
class BoardRsConnect(BaseBoard):

pins/constructors.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,18 @@ def board_github(
289289
)
290290

291291

292-
def board_urls(path: str, pin_paths: dict, cache=DEFAULT, allow_pickle_read=None):
292+
def board_urls(*args, **kwargs):
293+
"""DEPRECATED: This board has been renamed to board_url."""
294+
from .utils import warn_deprecated
295+
296+
warn_deprecated(
297+
"board_urls has been renamed to board_url. Please use board_url instead."
298+
)
299+
300+
return board_url(*args, **kwargs)
301+
302+
303+
def board_url(path: str, pin_paths: dict, cache=DEFAULT, allow_pickle_read=None):
293304
"""Create a board from individual urls.
294305
295306
Parameters
@@ -309,7 +320,7 @@ def board_urls(path: str, pin_paths: dict, cache=DEFAULT, allow_pickle_read=None
309320
... "df_csv": "df_csv/20220214T163720Z-9bfad/",
310321
... "df_arrow": "df_arrow/20220214T163720Z-ad0c1/",
311322
... }
312-
>>> board = board_urls(github_raw, pin_paths)
323+
>>> board = board_url(github_raw, pin_paths)
313324
>>> board.pin_list()
314325
['df_csv', 'df_arrow']
315326
"""
@@ -350,6 +361,31 @@ def board_rsconnect(
350361
CONNECT_API_KEY environment variable.
351362
**kwargs:
352363
Passed to the pins.board function.
364+
365+
Examples
366+
--------
367+
Use a server url or set the CONNECT_SERVER environt variable to connect:
368+
369+
::
370+
371+
server_url = "https://connect.rstudioservices.com"
372+
board = board_rsconnect(server_url)
373+
374+
In order to read a public pin, use board_manual with the public pin url.
375+
376+
::
377+
378+
# for a pin at https://connect.rstudioservices.com/content/3004/
379+
board = board_url(
380+
"https://connect.rstudioservices.com/content",
381+
{"my_df": "3004/"}
382+
)
383+
board.pin_read("my_df")
384+
385+
See Also
386+
--------
387+
board_url : board for connecting to individual pins, using a url or path.
388+
353389
"""
354390

355391
# TODO: api_key can be passed in to underlying RscApi, equiv to R's manual mode

pins/rsconnect/fs.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,6 @@ def put(
201201
except RsConnectApiMissingContentError:
202202
# TODO: this could be seen as analogous to mkdir (which gets
203203
# called by pins anyway)
204-
# TODO: hard-coded acl bad?
205204
content = self.api.post_content_item(parsed.content, access_type)
206205

207206
# Create bundle (with manifest.json inserted if missing) ----

pins/tests/test_boards.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,23 @@ def test_board_rsc_pin_write_acl(df, board_short):
443443
assert content["access_type"] == "all"
444444

445445

446+
@pytest.mark.fs_rsc
447+
def test_board_rsc_pin_read_public(df, board_short):
448+
from pins.boards import BoardManual
449+
450+
board_short.pin_write(df, "susan/mtcars", type="csv", access_type="all")
451+
452+
# note that users can also get this from the web ui
453+
content_url = board_short.fs.info("susan/mtcars")["content_url"]
454+
455+
# shouldn't be a key set in env, but remove just in case
456+
fs = fsspec.filesystem("http")
457+
board_url = BoardManual("", fs, pin_paths={"rsc_public": content_url})
458+
459+
df_no_key = board_url.pin_read("rsc_public")
460+
assert df_no_key.equals(df)
461+
462+
446463
# Manual Board Specific =======================================================
447464

448465
from pins.boards import BoardManual # noqa
@@ -491,3 +508,41 @@ def test_board_manual_pin_read():
491508

492509
# do a somewhat data-framey check
493510
assert df.shape[0] > 1
511+
512+
513+
def test_board_manual_construct_path():
514+
fs = fsspec.filesystem("file")
515+
root = "pins/tests/pins-compat"
516+
path_df_csv = "df_csv/20220214T163718Z-eceac/"
517+
path_df_csv_v2 = "df_csv/20220214T163720Z-9bfad/df_csv.csv"
518+
519+
board = BoardManual(
520+
root,
521+
fs,
522+
pin_paths={
523+
"df_csv": path_df_csv,
524+
"df_csv2_v2": path_df_csv_v2,
525+
},
526+
)
527+
528+
# path to pin folder ----
529+
# creates path to pin, ignores version, can include data.txt
530+
assert board.construct_path(["df_csv"]) == f"{root}/{path_df_csv}"
531+
assert board.construct_path(["df_csv", "v"]) == f"{root}/{path_df_csv}"
532+
assert (
533+
board.construct_path(["df_csv", "v", "data.txt"])
534+
== f"{root}/{path_df_csv}data.txt"
535+
)
536+
537+
with pytest.raises(NotImplementedError) as exc_info:
538+
board.construct_path(["df_csv", "v", "data.txt", "too_much"])
539+
540+
assert "Unable to construct path" in exc_info.value.args[0]
541+
542+
# path to individual file ----
543+
assert board.construct_path(["df_csv2_v2"]) == f"{root}/{path_df_csv_v2}"
544+
545+
with pytest.raises(ValueError) as exc_info:
546+
board.construct_path(["df_csv2_v2", "v"])
547+
548+
assert "assumed to be a single file" in exc_info.value.args[0]

0 commit comments

Comments
 (0)