11import tempfile
22import shutil
33import inspect
4+ import re
45
56from io import IOBase
67from 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
491533class 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
0 commit comments