Skip to content

Commit 9526472

Browse files
Make show_fits_image and show_table use file_input param
Generalise payload and title creation Docstrings updates
1 parent 889e1c7 commit 9526472

File tree

1 file changed

+156
-65
lines changed

1 file changed

+156
-65
lines changed

firefly_client/firefly_client.py

Lines changed: 156 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)