Skip to content

Commit 340d240

Browse files
authored
Merge pull request #29 from machow/feat-pin-search
Feat pin search
2 parents f50f976 + f2e10d8 commit 340d240

File tree

16 files changed

+197
-115
lines changed

16 files changed

+197
-115
lines changed

docs/api/api_card/pins.BaseBoard.pin_exists.rst

Lines changed: 0 additions & 6 deletions
This file was deleted.

docs/api/api_card/pins.BaseBoard.pin_list.rst

Lines changed: 0 additions & 6 deletions
This file was deleted.

docs/api/api_card/pins.BaseBoard.pin_meta.rst

Lines changed: 0 additions & 6 deletions
This file was deleted.

docs/api/api_card/pins.BaseBoard.pin_read.rst

Lines changed: 0 additions & 6 deletions
This file was deleted.

docs/api/api_card/pins.BaseBoard.pin_versions.rst

Lines changed: 0 additions & 6 deletions
This file was deleted.

docs/api/api_card/pins.BaseBoard.rst

Lines changed: 0 additions & 37 deletions
This file was deleted.

docs/api/boards.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Methods (Implemented)
2626
~BaseBoard.pin_version_delete
2727
~BaseBoard.pin_versions_prune
2828
~BaseBoard.pin_delete
29+
~BaseBoard.pin_search
2930

3031
Methods (Planned)
3132
-----------------
@@ -34,5 +35,4 @@ Methods (Planned)
3435

3536
~BaseBoard.pin_download
3637
~BaseBoard.pin_upload
37-
~BaseBoard.pin_search
3838
~BaseBoard.pin_browse

pins/boards.py

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import tempfile
22
import shutil
33
import inspect
4+
import re
45

56
from io import IOBase
67
from functools import cached_property
@@ -59,7 +60,7 @@ def pin_exists(self, name: str) -> bool:
5960
Pin name.
6061
"""
6162

62-
return self.fs.exists(self.path_to_pin(name))
63+
return self.fs.exists(self.construct_path([self.path_to_pin(name)]))
6364

6465
def pin_versions(self, name: str, as_df: bool = True) -> Sequence[VersionRaw]:
6566
"""Return available versions of a pin.
@@ -73,7 +74,7 @@ def pin_versions(self, name: str, as_df: bool = True) -> Sequence[VersionRaw]:
7374
if not self.pin_exists(name):
7475
raise PinsError("Cannot check version, since pin %s does not exist" % name)
7576

76-
versions_raw = self.fs.ls(self.path_to_pin(name))
77+
versions_raw = self.fs.ls(self.construct_path([self.path_to_pin(name)]))
7778

7879
# get a list of Version(Raw) objects
7980
all_versions = []
@@ -138,7 +139,7 @@ def pin_meta(self, name, version: str = None) -> Meta:
138139

139140
path_version = self.construct_path([*components, meta_name])
140141
f = self.fs.open(path_version)
141-
return self.meta_factory.read_yaml(f, selected_version)
142+
return self.meta_factory.read_pin_yaml(f, pin_name, selected_version)
142143

143144
def pin_list(self):
144145
"""List names of all pins in a board.
@@ -235,7 +236,7 @@ def pin_write(
235236

236237
# move pin to destination ----
237238
# create pin version folder
238-
dst_pin_path = self.path_to_pin(name)
239+
dst_pin_path = self.construct_path([self.path_to_pin(name)])
239240
dst_version_path = self.path_to_deploy_version(name, meta.version.version)
240241

241242
try:
@@ -360,8 +361,8 @@ def pin_versions_prune(
360361
for version in to_delete:
361362
self.pin_version_delete(name, version.version)
362363

363-
def pin_search(self, search=None):
364-
"""TODO: Search for pins.
364+
def pin_search(self, search=None, as_df=True):
365+
"""Search for pins.
365366
366367
The underlying search method depends on the board implementation, but most
367368
will search for text in the pin name and title.
@@ -370,12 +371,46 @@ def pin_search(self, search=None):
370371
----------
371372
search:
372373
A string to search for. By default returns all pins.
374+
as_df:
375+
Whether to return a pandas DataFrame.
373376
374377
"""
375-
raise NotImplementedError()
378+
379+
# fetch metadata ----
380+
381+
names = self.pin_list()
382+
383+
metas = list(map(self.pin_meta, names))
384+
385+
# search pins ----
386+
387+
if search:
388+
regex = re.compile(search) if isinstance(search, str) else search
389+
390+
res = []
391+
for meta in metas:
392+
if re.search(regex, meta.name) or re.search(regex, meta.title):
393+
res.append(meta)
394+
else:
395+
res = metas
396+
397+
# extract specific fields out ----
398+
399+
if as_df:
400+
# optionally pull out selected fields into a DataFrame
401+
import pandas as pd
402+
403+
# TODO(question): was the pulling of specific fields out a v0 thing?
404+
extracted = list(map(self._extract_meta_results, res))
405+
return pd.DataFrame(extracted)
406+
407+
# TODO(compat): double check on the as_df=True convention
408+
# TODO(compat): double check how people feel the dataframe display
409+
# looks with meta objects in it.
410+
return res
376411

377412
def pin_delete(self, names: "str | Sequence[str]"):
378-
"""TODO: Delete a pin (or pins), removing it from the board.
413+
"""Delete a pin (or pins), removing it from the board.
379414
380415
Parameters
381416
----------
@@ -390,8 +425,8 @@ def pin_delete(self, names: "str | Sequence[str]"):
390425
if not self.pin_exists(name):
391426
raise PinsError("Cannot delete pin, since pin %s does not exist" % name)
392427

393-
pin_name = self.path_to_pin(name)
394-
self.fs.rm(pin_name, recursive=True)
428+
path_to_pin = self.construct_path([self.path_to_pin(name)])
429+
self.fs.rm(path_to_pin, recursive=True)
395430

396431
def pin_browse(self, name, version=None, local=False):
397432
"""TODO: Navigate to the home of a pin, either on the internet or locally.
@@ -421,14 +456,14 @@ def validate_pin_name(self, name: str) -> None:
421456
def path_to_pin(self, name: str) -> str:
422457
self.validate_pin_name(name)
423458

424-
return self.construct_path([self.board, name])
459+
return name
425460

426461
def path_to_deploy_version(self, name: str, version: str):
427462
return self.construct_path([self.path_to_pin(name), version])
428463

429464
def construct_path(self, elements) -> str:
430465
# TODO: should be the job of IFileSystem?
431-
return "/".join(elements)
466+
return "/".join([self.board] + elements)
432467

433468
def keep_final_path_component(self, path):
434469
return path.split("/")[-1]
@@ -483,10 +518,17 @@ def prepare_pin_version(
483518
# write metadata to tmp pin folder
484519
meta_name = self.meta_factory.get_meta_name()
485520
src_meta_path = Path(pin_dir_path) / meta_name
486-
meta.to_yaml(src_meta_path.open("w"))
521+
meta.to_pin_yaml(src_meta_path.open("w"))
487522

488523
return meta
489524

525+
def _extract_search_meta(self, meta):
526+
keep_fields = ["name", "type", "title", "created", "file_size"]
527+
528+
d = {k: getattr(meta, k) for k in keep_fields}
529+
d["meta"] = meta
530+
return d
531+
490532

491533
class BoardRsConnect(BaseBoard):
492534
# TODO: note that board is unused in this class (e.g. it's not in construct_path())
@@ -509,6 +551,47 @@ def pin_list(self):
509551
names = [f"{cont['owner_username']}/{cont['name']}" for cont in results]
510552
return names
511553

554+
def pin_search(self, search=None, as_df=True):
555+
from pins.rsconnect.api import RsConnectApiRequestError
556+
557+
paged_res = self.fs.api.misc_get_applications("content_type:pin", search=search)
558+
results = paged_res.results
559+
names = [f"{cont['owner_username']}/{cont['name']}" for cont in results]
560+
561+
res = []
562+
for pin_name in names:
563+
try:
564+
meta = self.pin_meta(pin_name)
565+
res.append(meta)
566+
567+
except RsConnectApiRequestError as e:
568+
# handles the case where admins can search content they can't access
569+
# verify code is for inadequate permission to access
570+
if e.args[0]["code"] != 19:
571+
raise e
572+
# TODO(question): should this be a RawMeta class or something?
573+
# that fixes our isinstance Meta below.
574+
# TODO(compatibility): R pins errors instead, see #27
575+
res.append({"name": pin_name, "meta": None})
576+
577+
# extract specific fields out ----
578+
579+
if as_df:
580+
# optionally pull out selected fields into a DataFrame
581+
import pandas as pd
582+
583+
extract = []
584+
for entry in res:
585+
extract.append(
586+
self._extract_search_meta(entry)
587+
if isinstance(entry, Meta)
588+
else entry
589+
)
590+
591+
return pd.DataFrame(extract)
592+
593+
return res
594+
512595
def pin_version_delete(self, *args, **kwargs):
513596
from pins.rsconnect.api import RsConnectApiRequestError
514597

@@ -545,7 +628,11 @@ def path_to_pin(self, name: str) -> str:
545628
return name
546629

547630
# otherwise, prepend username to pin
548-
return self.construct_path([self.user_name, name])
631+
return f"{self.user_name}/{name}"
632+
633+
def construct_path(self, elements):
634+
# no need to prefix with board
635+
return "/".join(elements)
549636

550637
def path_to_deploy_version(self, name: str, version: str):
551638
# RSConnect deploys a content bundle for a new version, so we simply need

pins/meta.py

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ class Meta:
2121
A title for the pin.
2222
description:
2323
A detailed description of the pin contents.
24+
created:
25+
Datetime the pin was created (TODO: document format).
26+
pin_hash:
27+
A hash of the pin.
2428
file:
2529
All relevant files contained in the pin.
2630
file_size:
@@ -58,41 +62,29 @@ class Meta:
5862
name: Optional[str] = None
5963
user: Mapping = field(default_factory=dict)
6064

61-
def to_dict(self, flat=False, fmt_created=False) -> Mapping:
65+
def to_dict(self) -> Mapping:
6266
data = asdict(self)
6367

64-
if fmt_created:
65-
created_val = self.version.render_created()
66-
else:
67-
created_val = data["version"]["created"]
68-
69-
if not flat:
70-
if fmt_created:
71-
data["version"]["created"] = created_val
72-
return data
73-
else:
74-
flat_data = {k: v for k, v in data.items() if k != "version"}
75-
76-
flat_data["created"] = created_val
77-
flat_data["pin_hash"] = data["version"]["hash"]
78-
79-
return flat_data
68+
return data
8069

8170
def to_pin_dict(self):
82-
return self.to_dict(flat=True, fmt_created=True)
71+
d = self.to_dict()
72+
del d["name"]
73+
del d["version"]
74+
return d
8375

8476
@classmethod
85-
def from_pin_dict(cls, data, version) -> "Meta":
77+
def from_pin_dict(cls, data, pin_name, version) -> "Meta":
8678

8779
# version_fields = {"created", "pin_hash"}
8880

8981
# #get items necessary for re-creating meta data
9082
# meta_data = {k: v for k, v in data.items() if k not in version_fields}
9183
# version = version_cls.from_meta_fields(data["created"], data["pin_hash"])
92-
return cls(**data, version=version)
84+
return cls(**data, name=pin_name, version=version)
9385

94-
def to_yaml(self, f: Optional[IOBase] = None) -> "str | None":
95-
data = self.to_dict(flat=True, fmt_created=True)
86+
def to_pin_yaml(self, f: Optional[IOBase] = None) -> "str | None":
87+
data = self.to_pin_dict()
9688

9789
return yaml.dump(data, f)
9890

@@ -159,12 +151,14 @@ def create(
159151
version=version,
160152
)
161153

162-
def read_yaml(self, f: IOBase, version: "str | VersionRaw") -> Meta:
154+
def read_pin_yaml(
155+
self, f: IOBase, pin_name: str, version: "str | VersionRaw"
156+
) -> Meta:
163157
if isinstance(version, str):
164158
version_obj = guess_version(version)
165159
else:
166160
version_obj = version
167161

168162
data = yaml.safe_load(f)
169163

170-
return Meta.from_pin_dict(data, version=version_obj)
164+
return Meta.from_pin_dict(data, pin_name, version=version_obj)

pins/rsconnect/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ def misc_get_content_bundle_file(
403403
_download_file(r, f_obj)
404404

405405
def misc_get_applications(
406-
self, filter: str, count: int = 1000
406+
self, filter: str, count: int = 1000, search: str = None
407407
) -> Paginated[Sequence[Content]]:
408408
# TODO(question): R pins does not handle pagination, but we could do it here.
409409
# Note that R pins also just gets first 1000 entries

0 commit comments

Comments
 (0)