Skip to content

Commit 84c76da

Browse files
authored
Merge pull request #109 from machow/feat-rsc-access-type
Feat rsc access type
2 parents 0cc265e + 8710e5f commit 84c76da

File tree

7 files changed

+123
-8
lines changed

7 files changed

+123
-8
lines changed

pins/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@
2020
board_urls,
2121
board_rsconnect,
2222
board_s3,
23+
board,
2324
)

pins/boards.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .meta import Meta, MetaRaw, MetaFactory
1616
from .errors import PinsError
1717
from .drivers import load_data, save_data, default_title
18-
from .utils import inform
18+
from .utils import inform, ExtendMethodDoc
1919
from .config import get_allow_rsc_short_name
2020

2121

@@ -650,12 +650,15 @@ def __init__(self, *args, pin_paths: dict, **kwargs):
650650

651651
# return super().pin_read(*args, **kwargs)
652652

653+
@ExtendMethodDoc
653654
def pin_list(self):
654655
return list(self.pin_paths)
655656

657+
@ExtendMethodDoc
656658
def pin_versions(self, *args, **kwargs):
657659
raise NotImplementedError("This board does not support pin_versions.")
658660

661+
@ExtendMethodDoc
659662
def pin_meta(self, name, version=None):
660663
if version is not None:
661664
raise NotImplementedError()
@@ -680,6 +683,7 @@ def pin_meta(self, name, version=None):
680683

681684
return meta
682685

686+
@ExtendMethodDoc
683687
def pin_download(self, name, version=None, hash=None) -> Sequence[str]:
684688
meta = self.pin_meta(name, version)
685689

@@ -723,6 +727,7 @@ class BoardRsConnect(BaseBoard):
723727

724728
# defaults work ----
725729

730+
@ExtendMethodDoc
726731
def pin_list(self):
727732
# lists all pin content on RStudio Connect server
728733
# we can't use fs.ls, because it will list *all content*
@@ -732,7 +737,15 @@ def pin_list(self):
732737
names = [f"{cont['owner_username']}/{cont['name']}" for cont in results]
733738
return names
734739

735-
def pin_write(self, *args, **kwargs):
740+
@ExtendMethodDoc
741+
def pin_write(self, *args, access_type=None, **kwargs):
742+
"""Write a pin.
743+
744+
Extends parent method in the following ways:
745+
746+
* Modifies content item to include any title and description changes.
747+
* Adds access_type argument to specify who can see content. Defaults to "acl".
748+
"""
736749

737750
# run parent function ---
738751

@@ -757,6 +770,7 @@ def pin_write(self, *args, **kwargs):
757770

758771
return meta
759772

773+
@ExtendMethodDoc
760774
def pin_search(self, search=None, as_df=True):
761775
from pins.rsconnect.api import RsConnectApiRequestError
762776

@@ -793,6 +807,7 @@ def pin_search(self, search=None, as_df=True):
793807

794808
return res
795809

810+
@ExtendMethodDoc
796811
def pin_version_delete(self, *args, **kwargs):
797812
from pins.rsconnect.api import RsConnectApiRequestError
798813

@@ -804,6 +819,7 @@ def pin_version_delete(self, *args, **kwargs):
804819

805820
raise PinsError("RStudio Connect cannot delete the latest pin version.")
806821

822+
@ExtendMethodDoc
807823
def pin_versions_prune(self, *args, **kwargs):
808824
sig = inspect.signature(super().pin_versions_prune)
809825
if sig.bind(*args, **kwargs).arguments.get("days") is not None:

pins/constructors.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ def board(
7070
storage_options: "dict | None" = None,
7171
board_factory: "callable | BaseBoard | None" = None,
7272
):
73-
"""
73+
"""General function for constructing a pins board.
74+
75+
Note that this is a lower-level function. For most use cases, use a more specific
76+
function like board_local(...), or board_s3(...).
77+
7478
Parameters
7579
----------
7680
protocol:

pins/rsconnect/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ def get_content_item(self, guid: str) -> Content:
304304
return Content(result)
305305

306306
def post_content_item(
307-
self, name, access_type, title: str = "", description: str = "", **kwargs
307+
self, name, access_type: str, title: str = "", description: str = "", **kwargs
308308
) -> Content:
309309
data = self._get_params(locals(), exclude={"kwargs"})
310310
result = self.query_v1("content", "POST", json={**data, **kwargs})

pins/rsconnect/fs.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@
1818
RSC_CODE_OBJECT_DOES_NOT_EXIST,
1919
)
2020

21+
# Misc ----
22+
23+
24+
def _not_impl_args_kwargs(args, kwargs):
25+
return NotImplementedError(
26+
"Additional args and kwargs not supported." f"\nArgs: {args}\nKwargs: {kwargs}"
27+
)
28+
29+
30+
# Bundles ----
31+
2132

2233
@dataclass
2334
class PinBundleManifestMetadata:
@@ -148,6 +159,7 @@ def put(
148159
rpath,
149160
recursive=False,
150161
*args,
162+
access_type="acl",
151163
deploy=True,
152164
cls_manifest=PinBundleManifest,
153165
**kwargs,
@@ -160,6 +172,8 @@ def put(
160172
A path to the local bundle directory.
161173
rpath: str
162174
A path to the content where the bundle is being put.
175+
access_type:
176+
Who can use and view this content? Must be one of all, logged_in or acl.
163177
cls_manifest:
164178
If maniest does not exist, a class with an .add_manifest_to_directory()
165179
method.
@@ -168,6 +182,9 @@ def put(
168182

169183
parsed = self.parse_path(rpath)
170184

185+
if len(args) or len(kwargs):
186+
raise _not_impl_args_kwargs(args, kwargs)
187+
171188
if recursive is False:
172189
raise NotImplementedError(
173190
"Must set recursive to True in order to put any RSConnect content."
@@ -185,7 +202,7 @@ def put(
185202
# TODO: this could be seen as analogous to mkdir (which gets
186203
# called by pins anyway)
187204
# TODO: hard-coded acl bad?
188-
content = self.api.post_content_item(parsed.content, "acl")
205+
content = self.api.post_content_item(parsed.content, access_type)
189206

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

@@ -265,18 +282,22 @@ def exists(self, path: str, **kwargs) -> bool:
265282
except RsConnectApiMissingContentError:
266283
return False
267284

268-
def mkdir(self, path, create_parents=True, **kwargs) -> None:
285+
def mkdir(
286+
self, path, create_parents=True, *args, access_type="acl", **kwargs
287+
) -> None:
269288
parsed = self.parse_path(path)
270289

290+
if len(args) or len(kwargs):
291+
raise _not_impl_args_kwargs(args, kwargs)
292+
271293
if not isinstance(parsed, ContentPath):
272294
raise ValueError(f"Requires path to content, but received: {path}")
273295

274296
if self.exists(path):
275297
raise FileExistsError(path)
276298

277299
# TODO: could implement and call makedirs, but seems overkill
278-
# TODO: hard-coded "acl"?
279-
self.api.post_content_item(parsed.content, "acl", **kwargs)
300+
self.api.post_content_item(parsed.content, access_type, **kwargs)
280301

281302
def info(self, path, **kwargs) -> "User | Content | Bundle":
282303
# TODO: source of fsspec info uses self._parent to check cache?

pins/tests/test_rsconnect_api.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,25 @@ def test_rsconnect_fs_put_bundle(fs_short):
426426
assert f_index.read().decode() == (Path(path_to_example) / "index.html").read_text()
427427

428428

429+
def test_rsconnect_fs_put_bundle_all_access(fs_short):
430+
import requests
431+
432+
# TODO: use pkg_resources to get this
433+
path_to_example = "pins/tests/example-bundle"
434+
fs_short.put(
435+
path_to_example, "susan/test-content", recursive=True, access_type="all"
436+
)
437+
438+
# access control is set at the content (not bundle) level. we need to get the
439+
# content guid to recreate the "shareable" link
440+
content = fs_short.info("susan/test-content")
441+
442+
r = requests.get(f"{fs_short.api.server_url}/content/{content['guid']}")
443+
r.raise_for_status()
444+
445+
assert r.text == (Path(path_to_example) / "index.html").read_text()
446+
447+
429448
# fs.mkdir ----
430449

431450

pins/utils.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import sys
22

3+
from functools import update_wrapper
4+
from types import MethodType
5+
36
from .config import pins_options
47

58

@@ -9,3 +12,54 @@ def inform(log, msg):
912

1013
if not pins_options.quiet:
1114
print(msg, file=sys.stderr)
15+
16+
17+
class ExtendMethodDoc:
18+
# Note that the indentation assumes these are top-level method docstrings,
19+
# so are indented 8 spaces (after the initial sentence).
20+
template = """\
21+
{current_doc}
22+
23+
Parent method documentation:
24+
25+
{parent_doc}
26+
"""
27+
28+
def __init__(self, func):
29+
self.func = func
30+
31+
# allows sphinx to add the method signature to the docs
32+
# this is pretty benign, since it's very hard to call a descriptor
33+
# after class initialization (where __set_name__ is called).
34+
self.__call__ = func
35+
36+
def __set_name__(self, owner, name):
37+
bound_parent_meth = getattr(super(owner, owner), name)
38+
39+
self._parent_doc = bound_parent_meth.__doc__
40+
self._orig_doc = self.func.__doc__
41+
42+
if self._orig_doc is not None:
43+
# update the docstring of the subclass method to include parent doc.
44+
self.func.__doc__ = self.template.format(
45+
current_doc=self._orig_doc, parent_doc=self._parent_doc
46+
)
47+
48+
# make descriptor look like wrapped function
49+
update_wrapper(
50+
self, self.func, ("__doc__", "__name__", "__module__", "__qualname__")
51+
)
52+
53+
def __get__(self, obj, objtype=None):
54+
if obj is None:
55+
# accessing from class, return descriptor itself.
56+
return self
57+
58+
# accessing from instance
59+
return MethodType(self.func, obj)
60+
61+
def __call__(self, *args, **kwargs):
62+
# this is defined, so that callable(ExtendMethodDoc(...)) is True,
63+
# which allows all the inspect machinery to give sphinx the __call__
64+
# attribute we set in __init__.
65+
raise NotImplementedError()

0 commit comments

Comments
 (0)