Skip to content

Commit 7fca499

Browse files
authored
Merge pull request #61 from machow/fix-rsconnect-full
Fix rsconnect full
2 parents 26c6487 + 0263936 commit 7fca499

File tree

9 files changed

+163
-63
lines changed

9 files changed

+163
-63
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include pins/data/*.csv

pins/boards.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ def pin_search(self, search=None, as_df=True):
416416
import pandas as pd
417417

418418
# TODO(question): was the pulling of specific fields out a v0 thing?
419-
extracted = list(map(self._extract_meta_results, res))
419+
extracted = list(map(self._extract_search_meta, res))
420420
return pd.DataFrame(extracted)
421421

422422
# TODO(compat): double check on the as_df=True convention
@@ -540,7 +540,7 @@ def prepare_pin_version(
540540
def _extract_search_meta(self, meta):
541541
keep_fields = ["name", "type", "title", "created", "file_size"]
542542

543-
d = {k: getattr(meta, k) for k in keep_fields}
543+
d = {k: getattr(meta, k, None) for k in keep_fields}
544544
d["meta"] = meta
545545
return d
546546

@@ -593,7 +593,7 @@ def pin_meta(self, name, version=None):
593593
path_to_pin = self.construct_path([pin_name])
594594
if self.fs.protocol == "http" and not path_to_pin.rstrip().endswith("/"):
595595
# create metadata, rather than read from a file
596-
return self.meta_factory.create_raw(path_to_pin, type="file",)
596+
return self.meta_factory.create_raw(path_to_pin, type="file", name=pin_name)
597597

598598
path_meta = self.construct_path([pin_name, meta_name])
599599
f = self.fs.open(path_meta)
@@ -661,10 +661,8 @@ def pin_search(self, search=None, as_df=True):
661661
# verify code is for inadequate permission to access
662662
if e.args[0]["code"] != 19:
663663
raise e
664-
# TODO(question): should this be a MetaRaw class or something?
665-
# that fixes our isinstance Meta below.
666664
# TODO(compatibility): R pins errors instead, see #27
667-
res.append({"name": pin_name, "meta": None})
665+
res.append(self.meta_factory.create_raw(None, type=None, name=pin_name))
668666

669667
# extract specific fields out ----
670668

@@ -674,11 +672,7 @@ def pin_search(self, search=None, as_df=True):
674672

675673
extract = []
676674
for entry in res:
677-
extract.append(
678-
self._extract_search_meta(entry)
679-
if isinstance(entry, Meta)
680-
else entry
681-
)
675+
extract.append(self._extract_search_meta(entry))
682676

683677
return pd.DataFrame(extract)
684678

@@ -736,13 +730,15 @@ def user_name(self):
736730
user = self.fs.api.get_user()
737731
return user["username"]
738732

739-
def prepare_pin_version(self, pin_dir_path, x, *args, **kwargs):
740-
from jinja2 import Environment
733+
def prepare_pin_version(self, pin_dir_path, x, name: "str | None", *args, **kwargs):
741734

742-
env = Environment()
743-
template = env.from_string(self.html_template.read_text())
735+
# RSC pin names can have form <user_name>/<name>, but this will try to
736+
# create the object in a directory named <user_name>. So we grab just
737+
# the <name> part.
738+
if "/" in name:
739+
name = name.split("/")[-1]
744740

745-
meta = super().prepare_pin_version(pin_dir_path, x, *args, **kwargs)
741+
meta = super().prepare_pin_version(pin_dir_path, x, name, *args, **kwargs)
746742

747743
# copy in files needed by index.html ----------------------------------
748744
crnt_files = set([meta.file] if isinstance(meta.file, str) else meta.file)
@@ -788,6 +784,13 @@ def prepare_pin_version(self, pin_dir_path, x, *args, **kwargs):
788784
# TODO(compat): set display none in index.html
789785
context["data_preview"] = json.dumps({})
790786

787+
# render html template ----
788+
789+
from jinja2 import Environment
790+
791+
env = Environment()
792+
template = env.from_string(self.html_template.read_text())
793+
791794
rendered = template.render(context)
792795
(Path(pin_dir_path) / "index.html").write_text(rendered)
793796

pins/data/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,17 @@
1-
from siuba.data import mtcars # noqa
1+
from importlib_resources import files as _files
2+
3+
sources = {
4+
"mtcars": _files("pins") / "data/mtcars.csv",
5+
}
6+
7+
8+
def __dir__():
9+
return list(sources)
10+
11+
12+
def __getattr__(k):
13+
import pandas as pd
14+
15+
f_path = sources.get("mtcars")
16+
17+
return pd.read_csv(f_path)

pins/data/mtcars.csv

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb
2+
21,6,160,110,3.9,2.62,16.46,0,1,4,4
3+
21,6,160,110,3.9,2.875,17.02,0,1,4,4
4+
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1
5+
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1
6+
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2
7+
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1
8+
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4
9+
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2
10+
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2
11+
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4
12+
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4
13+
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3
14+
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3
15+
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3
16+
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4
17+
10.4,8,460,215,3,5.424,17.82,0,0,3,4
18+
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4
19+
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1
20+
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2
21+
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1
22+
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1
23+
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2
24+
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2
25+
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4
26+
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2
27+
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1
28+
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2
29+
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2
30+
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4
31+
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6
32+
15,8,301,335,3.54,3.57,14.6,0,1,5,8
33+
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2

pins/meta.py

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import ClassVar
12
from dataclasses import dataclass, asdict, field
23

34
import yaml
@@ -24,8 +25,9 @@ class MetaRaw:
2425
The type of pin data stored. This is used to determine how to read / write it.
2526
"""
2627

27-
file: Union[str, Sequence[str]]
28+
file: "str | Sequence[str] | None"
2829
type: str
30+
name: str
2931

3032

3133
@dataclass
@@ -57,7 +59,7 @@ class Meta:
5759
5860
"""
5961

60-
title: str
62+
title: Optional[str]
6163
description: Optional[str]
6264

6365
# TODO(defer): different from R pins, which has a local field
@@ -93,19 +95,58 @@ def to_pin_dict(self):
9395
@classmethod
9496
def from_pin_dict(cls, data, pin_name, version) -> "Meta":
9597

96-
# version_fields = {"created", "pin_hash"}
97-
98-
# #get items necessary for re-creating meta data
99-
# meta_data = {k: v for k, v in data.items() if k not in version_fields}
100-
# version = version_cls.from_meta_fields(data["created"], data["pin_hash"])
101-
return cls(**data, name=pin_name, version=version)
98+
# TODO: re-arrange Meta argument positions to reflect what's been
99+
# learned about default arguments. e.g. title was not used at some
100+
# point in api_version 1
101+
extra = {"title": None} if "title" not in data else {}
102+
return cls(**data, **extra, name=pin_name, version=version)
102103

103104
def to_pin_yaml(self, f: Optional[IOBase] = None) -> "str | None":
104105
data = self.to_pin_dict()
105106

106107
return yaml.dump(data, f)
107108

108109

110+
@dataclass
111+
class MetaV0:
112+
file: Union[str, Sequence[str]]
113+
type: str
114+
115+
description: str
116+
name: str
117+
118+
version: VersionRaw
119+
120+
# holds raw data.txt contents
121+
original_fields: dict = field(default_factory=dict)
122+
user: dict = field(default_factory=dict, init=False)
123+
124+
title: ClassVar[None] = None
125+
created: ClassVar[None] = None
126+
pin_hash: ClassVar[None] = None
127+
file_size: ClassVar[None] = None
128+
api_version: ClassVar[None] = None
129+
130+
def to_dict(self):
131+
return asdict(self)
132+
133+
@classmethod
134+
def from_pin_dict(cls, data, pin_name, version) -> "MetaV0":
135+
# could infer from dataclasses.fields(), but seems excessive.
136+
req_fields = {"type", "description", "name"}
137+
138+
req_inputs = {k: v for k, v in data.items() if k in req_fields}
139+
req_inputs["file"] = data["path"]
140+
141+
return cls(**req_inputs, name=pin_name, original_fields=data, version=version)
142+
143+
def to_pin_dict(self):
144+
raise NotImplementedError("v0 pins metadata are read only.")
145+
146+
def to_pin_yaml(self, *args, **kwargs):
147+
self.to_pin_dict()
148+
149+
109150
class MetaFactory:
110151
"""Responsible for creating and loading (e.g. from yaml) of meta objects.
111152
@@ -168,8 +209,8 @@ def create(
168209
version=version,
169210
)
170211

171-
def create_raw(self, files: Sequence[StrOrFile], type: str = "file",) -> MetaRaw:
172-
return MetaRaw(files, type)
212+
def create_raw(self, files: Sequence[StrOrFile], type: str, name: str) -> MetaRaw:
213+
return MetaRaw(files, type, name)
173214

174215
def read_pin_yaml(
175216
self, f: IOBase, pin_name: str, version: "str | VersionRaw"
@@ -181,4 +222,14 @@ def read_pin_yaml(
181222

182223
data = yaml.safe_load(f)
183224

184-
return Meta.from_pin_dict(data, pin_name, version=version_obj)
225+
api_version = data.get("api_version", 0)
226+
if api_version >= 2:
227+
raise NotImplementedError(
228+
f"api_version {api_version} by this version of the pins library"
229+
)
230+
elif api_version == 0:
231+
cls_meta = MetaV0
232+
else:
233+
cls_meta = Meta
234+
235+
return cls_meta.from_pin_dict(data, pin_name, version=version_obj)

pins/rsconnect/api.py

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,15 @@ class RsConnectApi:
133133
api_key: "str | None"
134134
server_url: "str"
135135

136-
def __init__(self, server_url, api_key=None):
136+
def __init__(
137+
self,
138+
server_url: "str | None",
139+
api_key: "str | None" = None,
140+
session: "requests.Session | None" = None,
141+
):
137142
self.server_url = server_url
138143
self.api_key = api_key
144+
self.session = requests.Session() if session is None else session
139145

140146
# utility functions -------------------------------------------------------
141147

@@ -165,10 +171,16 @@ def _get_api_key(self):
165171
if self.api_key is not None:
166172
return self.api_key
167173

168-
return os.environ[RSC_API_KEY]
174+
return os.environ.get(RSC_API_KEY)
169175

170176
def _get_headers(self):
171-
return {"Authorization": f"key {self._get_api_key()}"}
177+
api_key = self._get_api_key()
178+
rsc_xsrf = self.session.cookies.get("RSC-XSRF")
179+
180+
d_key = {"Authorization": f"key {api_key}"} if api_key is not None else {}
181+
d_rsc = {"X-RSC-XSRF": rsc_xsrf} if rsc_xsrf is not None else {}
182+
183+
return {**d_key, **d_rsc}
172184

173185
def _validate_json_response(self, data: "dict | list"):
174186
if isinstance(data, list):
@@ -209,7 +221,7 @@ def _raw_query(self, url, method="GET", return_request=False, **kwargs):
209221

210222
headers = self._get_headers()
211223

212-
r = requests.request(method, url, headers=headers, **kwargs)
224+
r = self.session.request(method, url, headers=headers, **kwargs)
213225

214226
if return_request:
215227
return r
@@ -418,46 +430,28 @@ def misc_get_applications(
418430

419431

420432
# ported from github.com/rstudio/connectapi
433+
# TODO: could just move these methods into RsConnectApi?
421434
class _HackyConnect(RsConnectApi):
422435
"""Handles logging in to connect, rather than using an API key.
423436
424437
This class allows you to create users and generate API keys on a fresh
425438
RStudio Connect service.
426439
"""
427440

428-
xsrf: "None | str"
429-
430-
def __init__(self, *args, **kwargs):
431-
self.xsrf = None
432-
super().__init__(*args, **kwargs)
433-
434-
def _get_headers(self):
435-
return {"X-RSC-XSRF": self.xsrf}
436-
437441
def login(self, user, password):
438442
res = self.query(
439443
"__login__",
440444
"POST",
441445
return_request=True,
442446
json={"username": user, "password": password},
443447
)
444-
self.xsrf = res.cookies["RSC-XSRF"]
445448
return res
446449

447450
def create_first_admin(self, user, password, email, keyname="first-key"):
448-
# TODO(question): this is run in the R rsconnect, but it returns json
449-
# error codes. tests run okay without it...
450-
# self.query_v1(
451-
# "users", "POST", json=dict(username=user, password=password, email=email)
452-
#
453-
# )
451+
self.login(user, password)
454452

455-
res = self.login(user, password)
453+
self.query("me")
456454

457-
self.query("me", cookies=res.cookies)
458-
459-
api_key = self.query(
460-
"keys", "POST", json=dict(name=keyname), cookies=res.cookies
461-
)
455+
api_key = self.query("keys", "POST", json=dict(name=keyname))
462456

463457
return RsConnectApi(self.server_url, api_key=api_key["key"])

pins/rsconnect/fs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from dataclasses import dataclass, asdict, field, fields
22
from pathlib import Path
33

4+
from fsspec import AbstractFileSystem
5+
46
from typing import Sequence
57

68
from .api import (
@@ -93,7 +95,7 @@ class BundleFilePath(BundlePath):
9395
file_name: str
9496

9597

96-
class RsConnectFs:
98+
class RsConnectFs(AbstractFileSystem):
9799
protocol: str = "rsc"
98100

99101
def __init__(self, server_url, **kwargs):

0 commit comments

Comments
 (0)