Skip to content

Commit 9ccfafe

Browse files
authored
Merge pull request #69 from machow/feat-rsc-title
feat(rsconnect): update pin title at content level
2 parents 6915e42 + b03d493 commit 9ccfafe

File tree

15 files changed

+255
-36
lines changed

15 files changed

+255
-36
lines changed

.github/workflows/ci.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,17 @@ on:
1111
jobs:
1212
tests:
1313
name: "Tests"
14-
runs-on: ubuntu-latest
14+
runs-on: ${{ matrix.os }}
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
include:
19+
- os: "macos-latest"
20+
# ignore doctests, as they involve calls to github, and all mac machines
21+
# use the same IP address
22+
pytest_opts: "-k pins/tests"
23+
- os: "ubuntu-latest"
24+
pytest_opts:
1525
steps:
1626
- uses: actions/checkout@v2
1727
- uses: actions/setup-python@v2
@@ -24,11 +34,12 @@ jobs:
2434
python -m pip install -e .
2535
- name: Run tests
2636
run: |
27-
pytest pins -m 'not fs_rsc and not skip_on_github'
37+
pytest pins -m 'not fs_rsc and not skip_on_github' $PYTEST_OPTS
2838
env:
2939
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
3040
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
3141
AWS_REGION: "us-east-1"
42+
PYTEST_OPTS: ${{ matrix.pytest_opts}}
3243

3344
test-rsconnect:
3445
name: "Test RSConnect"
@@ -49,6 +60,7 @@ jobs:
4960
make dev
5061
env:
5162
RSC_LICENSE: ${{ secrets.RSC_LICENSE }}
63+
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
5264

5365
# NOTE: edited to run checks for python package
5466
- name: Run tests

pins/boards.py

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -245,18 +245,29 @@ def pin_write(
245245
A date to store in the Meta.created field. This field may be used as
246246
part of the pin version name.
247247
"""
248+
249+
pin_name = self.path_to_pin(name)
250+
248251
# TODO(docs): describe options for type argument
249252
# TODO(docs): elaborate on default behavior for versioned parameter
250253
# TODO(compat): python pins added a created parameter above
251254
with tempfile.TemporaryDirectory() as tmp_dir:
252255
# create all pin data (e.g. data.txt, save object)
253256
meta = self.prepare_pin_version(
254-
tmp_dir, x, name, type, title, description, metadata, versioned, created
257+
tmp_dir,
258+
x,
259+
pin_name,
260+
type,
261+
title,
262+
description,
263+
metadata,
264+
versioned,
265+
created,
255266
)
256267

257268
# move pin to destination ----
258269
# create pin version folder
259-
dst_pin_path = self.construct_path([self.path_to_pin(name)])
270+
dst_pin_path = self.construct_path([pin_name])
260271
dst_version_path = self.path_to_deploy_version(name, meta.version.version)
261272

262273
try:
@@ -490,7 +501,7 @@ def keep_final_path_component(self, path):
490501

491502
def sort_pin_versions(self, versions):
492503
# assume filesystem returned them with most recent last
493-
return versions
504+
return sorted(versions, key=lambda v: v.version)
494505

495506
def prepare_pin_version(
496507
self,
@@ -523,10 +534,11 @@ def prepare_pin_version(
523534
p_obj = Path(pin_dir_path) / name
524535

525536
# file is saved locally in order to hash, calc size
526-
save_data(x, str(p_obj), type)
537+
file_names = save_data(x, str(p_obj), type)
527538

528539
meta = self.meta_factory.create(
529-
str(p_obj),
540+
pin_dir_path,
541+
file_names,
530542
type,
531543
title=title,
532544
description=description,
@@ -556,8 +568,8 @@ class BoardManual(BaseBoard):
556568
Examples
557569
--------
558570
>>> import fsspec
571+
>>> import os
559572
>>> fs = fsspec.filesystem("github", org = "machow", repo = "pins-python")
560-
561573
>>> pin_paths = {"df_csv": "df_csv/20220214T163720Z-9bfad"}
562574
>>> board = BoardManual("pins/tests/pins-compat", fs, pin_paths=pin_paths)
563575
@@ -650,6 +662,31 @@ def pin_list(self):
650662
names = [f"{cont['owner_username']}/{cont['name']}" for cont in results]
651663
return names
652664

665+
def pin_write(self, *args, **kwargs):
666+
667+
# run parent function ---
668+
669+
f_super = super().pin_write
670+
meta = f_super(*args, **kwargs)
671+
672+
# update content title to reflect what's in metadata ----
673+
674+
# TODO(question): R pins updates this info before writing the pin..?
675+
# bind the original signature to get pin name
676+
sig = inspect.signature(f_super)
677+
bind = sig.bind(*args, **kwargs)
678+
679+
pin_name = self.path_to_pin(bind.arguments["name"])
680+
content = self.fs.info(pin_name)
681+
self.fs.api.patch_content_item(
682+
content["guid"],
683+
title=meta.title,
684+
description=meta.description or "",
685+
# access_type = content.access_type
686+
)
687+
688+
return meta
689+
653690
def pin_search(self, search=None, as_df=True):
654691
from pins.rsconnect.api import RsConnectApiRequestError
655692

@@ -742,10 +779,12 @@ def prepare_pin_version(self, pin_dir_path, x, name: "str | None", *args, **kwar
742779
# RSC pin names can have form <user_name>/<name>, but this will try to
743780
# create the object in a directory named <user_name>. So we grab just
744781
# the <name> part.
745-
if "/" in name:
746-
name = name.split("/")[-1]
782+
short_name = name.split("/")[-1]
747783

748-
meta = super().prepare_pin_version(pin_dir_path, x, name, *args, **kwargs)
784+
# TODO(compat): py pins always uses the short name, R pins uses w/e the
785+
# user passed, but guessing people want the long name?
786+
meta = super().prepare_pin_version(pin_dir_path, x, short_name, *args, **kwargs)
787+
meta.name = name
749788

750789
# copy in files needed by index.html ----------------------------------
751790
crnt_files = set([meta.file] if isinstance(meta.file, str) else meta.file)
@@ -765,7 +804,8 @@ def prepare_pin_version(self, pin_dir_path, x, name: "str | None", *args, **kwar
765804
pin_files = ", ".join(f"""<a href="{x}">{x}</a>""" for x in all_files)
766805

767806
context = {
768-
"pin_name": "TODO",
807+
"date": meta.version.created.replace(microsecond=0),
808+
"pin_name": self.path_to_pin(name),
769809
"pin_files": pin_files,
770810
"pin_metadata": meta,
771811
}

pins/constructors.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,13 @@ def board_local(versioned=True, allow_pickle_read=None):
150150

151151

152152
def board_github(
153-
org, repo, path="", versioned=True, cache=DEFAULT, allow_pickle_read=None
153+
org,
154+
repo,
155+
path="",
156+
token=None,
157+
versioned=True,
158+
cache=DEFAULT,
159+
allow_pickle_read=None,
154160
):
155161
"""Returns a github pin board.
156162
@@ -162,6 +168,8 @@ def board_github(
162168
Name of the repo.
163169
path:
164170
A subfolder in the github repo holding the board.
171+
token:
172+
An optional github token.
165173
**kwargs:
166174
Passed to the pins.board function.
167175
@@ -173,6 +181,7 @@ def board_github(
173181
Examples
174182
--------
175183
184+
>>> import os
176185
>>> board = board_github("machow", "pins-python", "pins/tests/pins-compat")
177186
>>> board.pin_list()
178187
['df_arrow', 'df_csv', 'df_rds', 'df_unversioned']
@@ -241,7 +250,7 @@ def board_urls(path: str, pin_paths: dict, cache=DEFAULT, allow_pickle_read=None
241250

242251

243252
def board_rsconnect(
244-
versioned=True, server_url=None, api_key=None, cache=DEFAULT, allow_pickle_read=None
253+
server_url=None, versioned=True, api_key=None, cache=DEFAULT, allow_pickle_read=None
245254
):
246255
"""Create a board to read and write pins from an RStudio Connect instance.
247256

pins/drivers.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from .meta import Meta
77
from .errors import PinsInsecureReadError
88

9+
from typing import Sequence
10+
911
# TODO: move IFileSystem out of boards, to fix circular import
1012
# from .boards import IFileSystem
1113

@@ -69,7 +71,9 @@ def load_data(
6971
raise NotImplementedError(f"No driver for type {meta.type}")
7072

7173

72-
def save_data(obj, fname, type=None):
74+
def save_data(
75+
obj, fname, type=None, apply_suffix: bool = True
76+
) -> "str | Sequence[str]":
7377
# TODO: extensible saving with deferred importing
7478
# TODO: how to encode arguments to saving / loading drivers?
7579
# e.g. pandas index options
@@ -79,6 +83,9 @@ def save_data(obj, fname, type=None):
7983
if type == "csv":
8084
import pandas as pd
8185

86+
if apply_suffix:
87+
fname = f"{fname}.{type}"
88+
8289
if not isinstance(obj, pd.DataFrame):
8390
raise NotImplementedError(
8491
"Currently only pandas.DataFrame can be saved to a CSV."
@@ -87,10 +94,15 @@ def save_data(obj, fname, type=None):
8794
elif type == "joblib":
8895
import joblib
8996

97+
if apply_suffix:
98+
fname = f"{fname}.{type}"
99+
90100
joblib.dump(obj, fname)
91101
else:
92102
raise NotImplementedError(f"Cannot save type: {type}")
93103

104+
return fname
105+
94106

95107
def default_title(obj, type):
96108
if type == "csv":

pins/meta.py

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

45
import yaml
56

@@ -163,6 +164,7 @@ def get_version_for_meta(self, api_version) -> Version:
163164

164165
def create(
165166
self,
167+
base_folder: "str | Path",
166168
files: Sequence[StrOrFile],
167169
type,
168170
# TODO: when files is a string name should be okay as None
@@ -181,6 +183,8 @@ def create(
181183
version = Version.from_files([files], created)
182184
p_file = Path(files)
183185
file_size = p_file.stat().st_size
186+
file_name = str(Path(files).relative_to(Path(base_folder)))
187+
184188
elif isinstance(files, IOBase):
185189
# TODO: in theory can calculate size from a file object, but let's
186190
# wait until it's clear how calculating file size fits into pins
@@ -198,7 +202,7 @@ def create(
198202
return Meta(
199203
title=title,
200204
description=description,
201-
file=name, # TODO: FINISH
205+
file=file_name, # TODO: FINISH
202206
file_size=file_size,
203207
pin_hash=version.hash,
204208
created=version.render_created(),

pins/rsconnect/api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,11 @@ def post_content_item_deploy(self, guid: str, bundle_id: "str | None" = None):
314314
return self.query_v1(f"content/{guid}/deploy", "POST", json=json)
315315

316316
def patch_content_item(self, guid, **kwargs) -> Content:
317+
"""Update a content item (e.g. its title or description).
318+
319+
See post_content_item method for possible arguments.
320+
"""
321+
317322
# see https://docs.rstudio.com/connect/api/#patch-/v1/content/{guid}
318323
result = self.query_v1(f"content/{guid}", "PATCH", json=kwargs)
319324

pins/tests/_snapshots/test_board_pin_write_rsc_index_html/data.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
api_version: 1
22
created: 20200113T235859Z
33
description: some description
4-
file: test_rsc_pin
4+
file: test_rsc_pin.csv
55
file_size: 23
66
pin_hash: 60d4c36d7138cb6c
77
title: some pin

pins/tests/_snapshots/test_board_pin_write_rsc_index_html/index.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,21 @@
3030
<body>
3131

3232
<section>
33-
<h3>TODO</h3>
33+
<h3>derek/test_rsc_pin</h3>
3434

3535
<p>
36-
36+
<b>Last updated:</b> 2020-01-13 23:58:59 &bull;
3737
<b>Format:</b> csv &bull;
3838
<b>API:</b> v1
3939
</p>
4040
<p>some description</p>
41-
<p>Download data: <a href="test_rsc_pin">test_rsc_pin</a></p>
41+
<p>Download data: <a href="test_rsc_pin.csv">test_rsc_pin.csv</a></p>
4242
<details>
4343
<summary>Raw metadata</summary>
4444
<pre>api_version: 1
4545
created: 20200113T235859Z
4646
description: some description
47-
file: test_rsc_pin
47+
file: test_rsc_pin.csv
4848
file_size: 23
4949
pin_hash: 60d4c36d7138cb6c
5050
title: some pin
@@ -62,7 +62,7 @@ <h3>Code</h3>
6262
6363
<pre id="pin-r" class="pin-code"><code class="r">library(pins)
6464
board <-
65-
pin_read(board, "TODO")</code></pre>
65+
pin_read(board, "derek/test_rsc_pin")</code></pre>
6666
6767
!-->
6868

pins/tests/helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ def create_tmp_board(self, src_board=None):
181181

182182
shutil.copytree(src_board, p_root)
183183

184-
for pin_entry in p_root.glob("*/*"):
184+
# note that glob order is technically arbitrary
185+
for pin_entry in sorted(p_root.glob("*/*"), key=lambda x: str(x)):
185186
# two key points:
186187
# 1. username is required when putting content bundles up
187188
# 2. the version must be removed.

0 commit comments

Comments
 (0)