@@ -543,8 +543,8 @@ def upload_fits_data(self, stream):
543543
544544 Returns
545545 -------
546- out : `dict `
547- Status, like {'success': True} .
546+ out: `str `
547+ Path of file after the upload .
548548 """
549549 return self .upload_data (stream , 'FITS' )
550550
@@ -561,8 +561,8 @@ def upload_text_data(self, stream):
561561
562562 Returns
563563 -------
564- out : `dict `
565- Status, like {'success': True} .
564+ out: `str `
565+ Path of file after the upload .
566566 """
567567 return self .upload_data (stream , 'UNKNOWN' )
568568
@@ -580,8 +580,8 @@ def upload_data(self, stream, data_type):
580580
581581 Returns
582582 -------
583- out : `dict `
584- Status, like {'success': True} .
583+ out: `str `
584+ Path of file after the upload .
585585 """
586586
587587 url = self .url_cmd_service + '?cmd=upload&preload='
@@ -640,6 +640,49 @@ def dispatch(self, action_type, payload, override_channel=None):
640640 debug ('dispatch: type: %s, channel: %s \n %s' % (action_type , channel , dict_to_str (action )))
641641
642642 return self ._send_url_as_post (data )
643+
644+ def get_payload_from_file (self , file_input ):
645+ """Get payload for actions dispatched to Firefly server from the file input.
646+
647+ It does all guesswork to determine file type and uploads it to the server if needed.
648+
649+ Parameters
650+ ----------
651+ file_input : `str` or `file-like object`
652+ The input file to show. It can be a local file path, a URL to a
653+ remote file, name of a file on the server (return value of
654+ `upload_file()`), or a file-like object (such as an open IO stream).
655+
656+ Returns
657+ -------
658+ out : `dict`
659+ A dict that can be used as part of payload. The returned dict will
660+ have one of the following keys:
661+ - 'fileOnServer': `str` (server file reference for the uploaded file/stream)
662+ - 'url': `str` (for a remote file)
663+ """
664+ if isinstance (file_input , str ):
665+ if file_input .startswith ('${' ): # already a server file reference
666+ return {'fileOnServer' : file_input }
667+ elif os .path .isfile (file_input ): # local file path
668+ return {'fileOnServer' : self .upload_file (file_input )}
669+ elif re .match (r'^https?://' , file_input ): # remote file url
670+ return {'url' : file_input }
671+ elif isinstance (file_input , io .IOBase ) and hasattr (file_input , 'seek' ): # file-like object
672+ return {'fileOnServer' : self .upload_data (file_input , 'UNKNOWN' )}
673+ # invalid input
674+ raise ValueError ('file_input must be a valid file path string or a file-like object' )
675+
676+ def get_title_from_file (self , file_input ):
677+ """Get a title from a file input if possible."""
678+ if isinstance (file_input , str ):
679+ if os .path .isfile (file_input ):
680+ return os .path .basename (file_input )
681+ elif re .match (r'^https?://' , file_input ):
682+ url_basename = os .path .basename (file_input .split ('?' , 1 )[0 ]) # remove query parameters
683+ _ , ext = os .path .splitext (url_basename )
684+ return url_basename if ext else None
685+ return None
643686
644687 # -------------------------
645688 # dispatch actions
@@ -732,24 +775,8 @@ def reinit_viewer(self):
732775 Status of the request, like {'success': True}.
733776 """
734777 return self .dispatch (ACTION_DICT ['ReinitViewer' ], {})
735-
736- def get_file_payload (self , file_input ):
737- # TODO: debug displayName not appearing in some cases
738- if isinstance (file_input , str ):
739- if file_input .startswith ('$' ):
740- return {'fileOnServer' : file_input }
741- elif os .path .isfile (file_input ):
742- return {'fileOnServer' : self .upload_file (file_input ),
743- 'displayName' : os .path .basename (file_input )}
744- elif re .match (r'^https?://' , file_input ):
745- return {'url' : file_input ,
746- 'displayName' : os .path .basename (file_input )}
747- elif isinstance (file_input , io .IOBase ) and hasattr (file_input , 'seek' ): # file-like object
748- return {'fileOnServer' : self .upload_data (file_input , 'UNKNOWN' )}
749- raise ValueError ('file_input must be a valid file path string or a file-like object' )
750778
751- def show_data (self , file_input , preview_metadata = False ,
752- ** data_view_options ):
779+ def show_data (self , file_input , preview_metadata = False , title = None ):
753780 """
754781 Show any data file of the type that Firefly supports:
755782 - Custom catalog or table in IPAC, CSV, TSV, VOTABLE, Parquet, or FITS table format
@@ -762,68 +789,92 @@ def show_data(self, file_input, preview_metadata=False,
762789 Parameters
763790 ----------
764791 file_input : `str` or `file-like object`
765- If `str`, it can be a local file path, a URL to a remote file, or
766- name of a file on the server (return value of `upload_file()`).
767- Else it can be a file stream object (file-like object) opened in Python .
792+ The input file to show. It can be a local file path, a URL to a
793+ remote file, name of a file on the server (return value of
794+ `upload_file()`), or a file-like object (such as an open IO stream) .
768795 preview_metadata : `bool`, optional
769796 If True, preview the metadata of the file before loading the data.
770797 This allows you to select FITS extensions, table columns, etc. and
771798 then you can click "Load" button in the UI to load your selections.
772799 Default is False.
773- **data_view_options : optional keyword arguments
774- Additional options for the view of this data file in the UI, such as:
775- **displayName** : `str`, optional
776- A name to display in the UI for the data file loaded.
800+ title : `str`, optional
801+ Title to display with the data view in the UI. If not provided,
802+ will be derived from the file name if possible.
777803 """
778804 payload = {
779- ** self .get_file_payload (file_input ),
805+ ** self .get_payload_from_file (file_input ),
780806 'immediate' : not preview_metadata ,
781- ** data_view_options # TODO: pass data_view_id -> didn't work
807+ 'displayName' : self .get_title_from_file (file_input ) if not title else title ,
808+ # TODO: support optional keyword arguments like data_view_id, image/table/region options, etc.
809+ # **data_view_options
782810 }
783811 return self .dispatch (ACTION_DICT ['ShowAnyData' ], payload )
784812
785- def show_fits_image (self , file_on_server = None , plot_id = None , viewer_id = None , ** additional_params ):
813+ def show_fits_image (self , file_input = None , file_on_server = None , url = None ,
814+ plot_id = None , viewer_id = None , ** additional_params ):
786815 """
787- Show a FITS image. If the FITS file has multiple extensions/HDUs (for
816+ Show a FITS image.
817+
818+ If the FITS file has multiple extensions/HDUs (for
788819 images, tables, etc.), all the image extensions are displayed by default
789- within the image plot.
820+ within the image plot. You can use the `multiImageIdx` parameter
821+ to display only a particular image extension from the file.
790822
791823 Parameters
792824 ----------
825+ file_input : `str` or `file-like object`, optional
826+ The input file to show. It can be a local file path, a URL to a
827+ remote file, name of a file on the server (return value of
828+ `upload_file()`), or a file-like object (such as an open IO stream).
793829 file_on_server : `str`, optional
794830 The is the name of the file on the server.
795831 If you use `upload_file()`, then it is the return value of the method. Otherwise it is a file that
796832 Firefly has direct access to.
833+
834+ .. note:: Ignored if `file_input` is provided, which can automatically
835+ upload a local file and use its name on the server.
836+ url : `str`, optional
837+ URL of the FITS image file, if it's not a local file you can upload.
838+
839+ .. note:: Ignored if `file_input` is provided, which can automatically
840+ infer if a URL is passed to it.
797841 plot_id : `str`, optional
798842 The ID you assign to the image plot. This is necessary to further control the plot.
799843 viewer_id : `str`, optional
800844 The ID you assign to the viewer (or cell) used to contain the image plot. If grid view is used for
801845 display, the viewer id is the cell id of the cell which contains the image plot.
802846
847+ .. note:: Not needed in triview mode of Firefly, which is also
848+ the default view mode.
849+
803850 **additional_params : optional keyword arguments
804- Any valid fits viewer plotting parameter, please see the details in `FITS plotting parameters`_.
851+ Any valid fits viewer plotting parameter, please see the details in
852+ `FITS plotting parameters`_ (note that they are case-insensitive).
853+ These also allow you to directly show an image retrieved from a
854+ data service rather than from a file input, as explained in `this section`.
805855
806856 .. _`FITS plotting parameters`:
807857 https://github.com/Caltech-IPAC/firefly/blob/dev/docs/fits-plotting-parameters.md
858+ .. _`this section`:
859+ https://github.com/Caltech-IPAC/firefly/blob/dev/docs/fits-plotting-parameters.md#parameters-for-specifying-fits-files-retrieved-from-a-service
808860
809861 More options are shown as below:
810862
811- **MultiImageIdx ** : `int`, optional
863+ **multiImageIdx ** : `int`, optional
812864 Display only a particular image extension from the file (zero-based index).
813- **Title ** : `str`, optional
865+ **title ** : `str`, optional
814866 Title to display with the image.
815- **url** : `str`, optional
816- URL of the fits image file, if it's not a local file you can upload.
817867
818868 Returns
819869 -------
820870 out : `dict`
821871 Status of the request, like {'success': True}.
822872
823- .. note:: Either `file_on_server` or the target information set by `additional_params`
824- is used for image search.
873+ .. note:: `file_input`, `url`, `file_on_server`, and service specific `additional_params` are exclusively required.
874+ If more than one of these 4 parameters are passed, precedence order is:
875+ service specific `additional_params` > (`file_input` > `file_on_server` > `url`).
825876 """
826-
877+ # WebPlotRequest parameters
827878 wp_request = {'plotGroupId' : 'groupFromPython' ,
828879 'GroupLocked' : False }
829880 payload = {'wpRequest' : wp_request ,
@@ -836,7 +887,19 @@ def show_fits_image(self, file_on_server=None, plot_id=None, viewer_id=None, **a
836887
837888 payload .update ({'viewerId' : viewer_id })
838889 plot_id and payload ['wpRequest' ].update ({'plotId' : plot_id })
890+
891+ # Handle different params of file input
839892 file_on_server and payload ['wpRequest' ].update ({'file' : file_on_server })
893+ url and payload ['wpRequest' ].update ({'url' : url })
894+ if file_input :
895+ file_payload = self .get_payload_from_file (file_input )
896+ if 'fileOnServer' in file_payload :
897+ # WebPlotRequest uses 'file' as the key for the file on server
898+ file_payload ['file' ] = file_payload .pop ('fileOnServer' )
899+ payload ['wpRequest' ].update (file_payload ) # overrides url or file_on_server if any
900+
901+ # Add the additional params including the image view related options, and
902+ # service specific options (which implicitly overrides the above file params if any, when WebPlotRequest is processed)
840903 additional_params and payload ['wpRequest' ].update (additional_params )
841904
842905 r = self .dispatch (ACTION_DICT ['ShowImage' ], payload )
@@ -892,20 +955,31 @@ def show_fits_3color(self, three_color_params, plot_id=None, viewer_id=None):
892955 warning and r .update ({'warning' : warning })
893956 return r
894957
895- def show_table (self , file_on_server = None , url = None , tbl_id = None , title = None , page_size = 100 , is_catalog = True ,
958+ def show_table (self , file_input = None , file_on_server = None , url = None ,
959+ tbl_id = None , title = None , page_size = 100 , is_catalog = True ,
896960 meta = None , target_search_info = None , options = None , table_index = None ,
897961 column_spec = None , filters = None , visible = True ):
898962 """
899963 Show a table.
900964
901965 Parameters
902966 ----------
967+ file_input : `str` or `file-like object`, optional
968+ The input file to show. It can be a local file path, a URL to a
969+ remote file, name of a file on the server (return value of
970+ `upload_file()`), or a file-like object (such as an open IO stream).
903971 file_on_server : `str`, optional
904972 The name of the file on the server.
905973 If you use `upload_file()`, then it is the return value of the method. Otherwise it is a file that
906974 Firefly has direct access to.
975+
976+ .. note:: Ignored if `file_input` is provided, which can automatically
977+ upload a local file and use its name on the server.
907978 url : `str`, optional
908979 URL of the table file, if it's not a local file you can upload.
980+
981+ .. note:: Ignored if `file_input` is provided, which can automatically
982+ infer if a URL is passed to it.
909983 tbl_id : `str`, optional
910984 A table ID. It will be created automatically if not specified.
911985 title : `str`, optional
@@ -919,37 +993,37 @@ def show_table(self, file_on_server=None, url=None, tbl_id=None, title=None, pag
919993 target_search_info : `dict`, optional
920994 The information for target search, it may contain the following fields:
921995
922- **catalogProject** : `str`
996+ - **catalogProject** : `str`
923997 Catalog project, such as *'WISE'*.
924- **catalog** : `str`
998+ - **catalog** : `str`
925999 Table to be searched, such as *'allwise_p3as_psd'*.
926- **use** : `str`
1000+ - **use** : `str`
9271001 Usage of the table search, such as *'catalog_overlay'*.
928- **position** : `str`
1002+ - **position** : `str`
9291003 Target position, such as *'10.68479;41.26906;EQ_J2000'*.
930- **SearchMethod** : {'Cone', 'Eliptical', 'Box', 'Polygon', 'Table', 'AllSky'}
1004+ - **SearchMethod** : {'Cone', 'Eliptical', 'Box', 'Polygon', 'Table', 'AllSky'}
9311005 Target search method.
932- **radius** : `float`
1006+ - **radius** : `float`
9331007 The radius for *'Cone'* or the semi-major axis for *'Eliptical'* search in terms of unit *arcsec*.
934- **posang** : `float`
1008+ - **posang** : `float`
9351009 Position angle for *'Eliptical'* search in terms of unit *arcsec*.
936- **ratio** : `float`
1010+ - **ratio** : `float`
9371011 Axial ratio for *'Eliptical'* search.
938- **size** : `float`
1012+ - **size** : `float`
9391013 Side size for *'Box'* search in terms of unit *arcsec*.
940- **polygon** : `str`
1014+ - **polygon** : `str`
9411015 ra/dec of polygon corners, such as *'ra1, dec1, ra2, dec2,... raN, decN'*.
942- **filename** : `str`
1016+ - **filename** : `str`
9431017 The name of file on server on multi-objects for *'Table'* search.
9441018
9451019 options : `dict`, optional
946- Containing parameters for table display, such as,
1020+ Containing parameters for table display, such as:
9471021
948- **removable** : `bool`
1022+ - **removable** : `bool`
9491023 if table is removable.
950- **showUnits** : `bool`
1024+ - **showUnits** : `bool`
9511025 if table shows units for the columns.
952- **showFilters** : `bool`
1026+ - **showFilters** : `bool`
9531027 if table shows filter button
9541028 table_index : `int`, optional
9551029 The table to be shown in case `file_on_server` contains multiple tables. It is the extension number for
@@ -971,23 +1045,38 @@ def show_table(self, file_on_server=None, url=None, tbl_id=None, title=None, pag
9711045 out : `dict`
9721046 Status of the request, like {'success': True}.
9731047
974- .. note:: `url`, `file_on_server`, and `target_search_info` are exclusively required.
975- If more than one of these 3 parameters are passed, precedence order is:
976- `url ` > `file_on_server` > `target_search_info`
1048+ .. note:: `file_input`, ` url`, `file_on_server`, and `target_search_info` are exclusively required.
1049+ If more than one of these 4 parameters are passed, precedence order is:
1050+ (`file_input ` > `file_on_server` > `url`) > `target_search_info`
9771051 """
1052+ has_file_input = bool (file_on_server ) or bool (url ) or bool (file_input )
9781053
9791054 if not tbl_id :
9801055 tbl_id = gen_item_id ('Table' )
9811056 if not title :
982- title = tbl_id if file_on_server or url else target_search_info .get ('catalog' , tbl_id )
1057+ if has_file_input :
1058+ title_from_file = self .get_title_from_file (file_input )
1059+ title = title_from_file if title_from_file else tbl_id
1060+ else :
1061+ title = target_search_info .get ('catalog' , tbl_id )
9831062
9841063 meta_info = {'title' : title , 'tbl_id' : tbl_id }
9851064 meta and meta_info .update (meta )
9861065
9871066 tbl_req = {'startIdx' : 0 , 'pageSize' : page_size , 'tbl_id' : tbl_id }
988- if file_on_server or url :
1067+ if has_file_input :
9891068 tbl_type = 'table' if not is_catalog else 'catalog'
990- source = url if url else file_on_server
1069+
1070+ # Handle different params of file input
1071+ source = None
1072+ if file_input :
1073+ file_payload = self .get_payload_from_file (file_input )
1074+ source = file_payload .get ('fileOnServer' ) or file_payload .get ('url' )
1075+ elif file_on_server :
1076+ source = file_on_server
1077+ elif url :
1078+ source = url
1079+
9911080 tbl_req .update ({'source' : source , 'tblType' : tbl_type ,
9921081 'id' : 'IpacTableFromSource' })
9931082 table_index and tbl_req .update ({'tbl_index' : table_index })
@@ -997,6 +1086,8 @@ def show_table(self, file_on_server=None, url=None, tbl_id=None, title=None, pag
9971086 tbl_req .update ({'id' : 'GatorQuery' , 'UserTargetWorldPt' : target_search_info .get ('position' )})
9981087 target_search_info .pop ('position' , None )
9991088 tbl_req .update (target_search_info )
1089+ else :
1090+ raise ValueError ('Either file_input/file_on_server/url or target_search_info parameter is required.' )
10001091
10011092 tbl_req .update ({'META_INFO' : meta_info })
10021093 options and tbl_req .update ({'options' : options })
0 commit comments