Skip to content

Commit ee580dd

Browse files
authored
feat: rsc metadata local field with url, content guid (#167)
* feat: rsc metadata local field with url, content guid * ci: pin to pytest 7.1.3 to fix pytest-parallel * fix: include local url on rsconnect * docs: document local in meta docstring
1 parent 3907a91 commit ee580dd

File tree

4 files changed

+67
-15
lines changed

4 files changed

+67
-15
lines changed

pins/boards.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,11 @@ def pin_meta(self, name, version: str = None) -> Meta:
158158
meta_name = self.meta_factory.get_meta_name(*components)
159159

160160
path_meta = self.construct_path([*components, meta_name])
161-
f = self._open_pin_meta(path_meta)
161+
f, local = self._open_pin_meta(path_meta)
162162

163-
meta = self.meta_factory.read_pin_yaml(f, pin_name, selected_version)
163+
meta = self.meta_factory.read_pin_yaml(
164+
f, pin_name, selected_version, local=local
165+
)
164166

165167
return meta
166168

@@ -605,7 +607,10 @@ def _open_pin_meta(self, path):
605607
f = self.fs.open(path)
606608
self._touch_cache(path)
607609

608-
return f
610+
# optional additional data to put in Meta.local
611+
local = {}
612+
613+
return f, local
609614

610615
def _get_cache_path(self, pin_name, version=None, fname=None):
611616
version_part = [version] if version is not None else []
@@ -691,8 +696,8 @@ def pin_meta(self, name, version=None):
691696
# note that pins on this board should point to versions, so we use an
692697
# empty string to mark version (it ultimately is ignored)
693698
path_meta = self.construct_path([pin_name, "", meta_name])
694-
f = self._open_pin_meta(path_meta)
695-
meta = self.meta_factory.read_pin_yaml(f, pin_name, VersionRaw(""))
699+
f, local = self._open_pin_meta(path_meta)
700+
meta = self.meta_factory.read_pin_yaml(f, pin_name, VersionRaw(""), local=local)
696701

697702
# TODO(#59,#83): handle caching, and then re-enable pin_read.
698703
# self._touch_cache(path_meta)
@@ -866,6 +871,23 @@ def pin_versions_prune(self, *args, **kwargs):
866871
)
867872
super().pin_versions_prune(*args, **kwargs)
868873

874+
def _open_pin_meta(self, path):
875+
f = self.fs.open(path)
876+
self._touch_cache(path)
877+
878+
# optional additional data to put in Meta.local
879+
user_name, content_name, bundle_id = str(path).split("/")[:3]
880+
user_guid = self.fs._user_name_cache[user_name]
881+
content_guid = self.fs._content_name_cache[(user_guid, content_name)]
882+
883+
local = {
884+
"content_id": content_guid,
885+
"version": bundle_id,
886+
"url": f"{self.fs.api.server_url}/content/{content_guid}/",
887+
}
888+
889+
return f, local
890+
869891
def validate_pin_name(self, name) -> None:
870892
# this should be the default behavior, expecting a full pin name.
871893
# but because the tests use short names, we allow it to be disabled via config

pins/meta.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class Meta:
5757
TODO - where is this in R pins?
5858
user:
5959
A dictionary of additional metadata that may be specified by the user.
60+
local:
61+
A dictionary of additional metadata that may be added by the board, depending
62+
on the backend used. E.g. RStudio Connect content id, url, etc..
6063
6164
"""
6265

@@ -81,6 +84,7 @@ class Meta:
8184

8285
name: Optional[str] = None
8386
user: Mapping = field(default_factory=dict)
87+
local: Mapping = field(default_factory=dict)
8488

8589
def to_dict(self) -> Mapping:
8690
data = asdict(self)
@@ -89,18 +93,22 @@ def to_dict(self) -> Mapping:
8993

9094
def to_pin_dict(self):
9195
d = self.to_dict()
96+
9297
del d["name"]
9398
del d["version"]
99+
del d["local"]
100+
94101
return d
95102

96103
@classmethod
97-
def from_pin_dict(cls, data, pin_name, version) -> "Meta":
104+
def from_pin_dict(cls, data, pin_name, version, local=None) -> "Meta":
98105

99106
# TODO: re-arrange Meta argument positions to reflect what's been
100107
# learned about default arguments. e.g. title was not used at some
101108
# point in api_version 1
102109
extra = {"title": None} if "title" not in data else {}
103-
return cls(**data, **extra, name=pin_name, version=version)
110+
local = {} if local is None else local
111+
return cls(**data, **extra, name=pin_name, version=version, local=local)
104112

105113
def to_pin_yaml(self, f: Optional[IOBase] = None) -> "str | None":
106114
data = self.to_pin_dict()
@@ -121,6 +129,7 @@ class MetaV0:
121129
# holds raw data.txt contents
122130
original_fields: dict = field(default_factory=dict)
123131
user: dict = field(default_factory=dict, init=False)
132+
local: Mapping = field(default_factory=dict)
124133

125134
title: ClassVar[None] = None
126135
created: ClassVar[None] = None
@@ -132,15 +141,22 @@ def to_dict(self):
132141
return asdict(self)
133142

134143
@classmethod
135-
def from_pin_dict(cls, data, pin_name, version) -> "MetaV0":
144+
def from_pin_dict(cls, data, pin_name, version, local=None) -> "MetaV0":
136145
# could infer from dataclasses.fields(), but seems excessive.
137146
req_fields = {"type", "description"}
138147

139148
# Note that we need to .get(), since fields may not be in metadata
140149
req_inputs = {k: data.get(k) for k in req_fields}
141150
req_inputs["file"] = data["path"]
142151

143-
return cls(**req_inputs, name=pin_name, original_fields=data, version=version)
152+
local = {} if local is None else local
153+
return cls(
154+
**req_inputs,
155+
name=pin_name,
156+
original_fields=data,
157+
version=version,
158+
local=local,
159+
)
144160

145161
def to_pin_dict(self):
146162
raise NotImplementedError("v0 pins metadata are read only.")
@@ -216,7 +232,11 @@ def create_raw(self, files: Sequence[StrOrFile], type: str, name: str) -> MetaRa
216232
return MetaRaw(files, type, name)
217233

218234
def read_pin_yaml(
219-
self, f: IOBase, pin_name: str, version: "str | VersionRaw"
235+
self,
236+
f: IOBase,
237+
pin_name: str,
238+
version: "str | VersionRaw",
239+
local=None,
220240
) -> Meta:
221241
if isinstance(version, str):
222242
version_obj = guess_version(version)
@@ -235,4 +255,4 @@ def read_pin_yaml(
235255
else:
236256
cls_meta = Meta
237257

238-
return cls_meta.from_pin_dict(data, pin_name, version=version_obj)
258+
return cls_meta.from_pin_dict(data, pin_name, version=version_obj, local=local)

pins/rsconnect/fs.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ def __init__(self, server_url, **kwargs):
115115
else:
116116
self.api = RsConnectApi(server_url, **kwargs)
117117

118+
self._user_name_cache = {}
119+
self._content_name_cache = {}
120+
118121
def ls(
119122
self, path, details=False, **kwargs
120123
) -> "Sequence[BaseEntity] | Sequence[str]":
@@ -390,7 +393,11 @@ def _get_content_from_name(self, user_guid, content_name):
390393
raise err(
391394
f"Expecting 1 content entry, but found {len(contents)}: {contents}"
392395
)
393-
return contents[0]
396+
397+
res = contents[0]
398+
399+
self._content_name_cache[(user_guid, content_name)] = res["guid"]
400+
return res
394401

395402
def _get_content_bundle(self, content_guid, bundle_id):
396403
"""Fetch a content bundle."""
@@ -410,7 +417,10 @@ def _get_user_from_name(self, name):
410417
"""Fetch a single user entity from user name."""
411418
users = self.api.get_users(prefix=name)
412419
try:
413-
user_guid = next(iter([x for x in users if x["username"] == name]))
414-
return user_guid
420+
user = next(iter([x for x in users if x["username"] == name]))
421+
422+
self._user_name_cache[user["username"]] = user["guid"]
423+
424+
return user
415425
except StopIteration:
416426
raise ValueError(f"No user named {name} found.")

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ doc =
5050

5151
test =
5252
pip-tools
53-
pytest
53+
pytest==7.1.3
5454
pytest-cases
5555
pytest-dotenv
5656
pytest-parallel

0 commit comments

Comments
 (0)