diff --git a/README.md b/README.md index 91519875ab1656c88dc38acb42a5a1846a85507b..71caa3845c6101d221727319caccc8d23acc0373 100644 --- a/README.md +++ b/README.md @@ -2,46 +2,19 @@ A Python client for the ESCAPE ESAP User Profile REST API. -The `shopping_client` module, which communicates with the ESCAPE ESAP User Profile REST API is very lightweight. Archive-specific functionality is delegated to "connector" modules like the `zooniverse` module. +The `ShoppingClient` class, which communicates with the ESCAPE ESAP User Profile REST API, is very lightweight. Archive-specific functionality is delegated to "connector" classes like `Zooniverse` and `Alta`. ### Installation -The client and the Zooniverse client cat be installed using pip: +The client and a selection of connector classes client cat be installed using pip: ```sh $ pip install git+https://git.astron.nl/astron-sdc/esap-userprofile-python-client.git ``` -### Example - Using the Shopping Client with the Zooniverse connector +### Example -```python -from shopping_client import shopping_client -from zooniverse import zooniverse -import getpass - -# Prompt for Zooniverse account password -zooniverse_password = getpass.getpass("Enter Zooniverse password:") - -# Instantiate Zooniverse connector -zc = zooniverse(username="hughdickinson", password=zooniverse_password) - -# Instantiate ESAP User Profile shopping client, passing zooniverse connector -sc = shopping_client(host="https://sdc-dev.astron.nl:5555/", connectors=[zc]) - -# Retrieve basket (prompts to enter access token obtained from ESAP GUI) -res=sc.get_basket(convert_to_pandas=True) - -# ... inspect available results ... - -# Retrieve data from Zooniverse based on basket item -data = zc.retrieve(res["zooniverse"].loc[3], - generate=False, - wait=True, - convert_to_pandas=True) - -# ... analyse data ... - -``` +See `example.py`. ## Contributing diff --git a/__init__.py b/__init__.py deleted file mode 100644 index adaa6db6e1657e72679abe29edd50933e6dd5416..0000000000000000000000000000000000000000 --- a/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .zooniverse import zooniverse -from .shopping_client import shopping_client diff --git a/alta/__init__.py b/alta/__init__.py deleted file mode 100644 index 93ccfeb7fb70ca0134a242efb6d03e3837e5e065..0000000000000000000000000000000000000000 --- a/alta/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from alta.alta_connector import alta_connector diff --git a/alta/alta_connector.py b/alta/alta_connector.py deleted file mode 100644 index c514c2697f2e0c252167d62a8c3c15fa8653a1da..0000000000000000000000000000000000000000 --- a/alta/alta_connector.py +++ /dev/null @@ -1,76 +0,0 @@ -import requests -import json -import io -import getpass -import pandas as pd - -from typing import Union, Optional - -class alta_connector: - - name = "alta" - archive = "apertif" - - def basket_item_to_pandas( - self, basket_item: Union[dict, pd.Series], validate: bool = True - ) -> Optional[pd.Series]: - """Convert an item from the shopping basket into a `pd.Series` with - optional validation. - - Parameters - ---------- - basket_item : Union[dict, pd.Series] - A single item from a retrieved shopping basket - either a raw `dict` - or a converted `pd.Series`. - validate : bool - If `True`, check that the data in the shopping item conforms with - the expected format before attempting the conversion. - - Returns - ------- - Optional[pd.Series] - `pd.Series` containing the data encoded in the shopping item or - `NoneType`. - - """ - if validate: - item_data = self.validate_basket_item(basket_item, return_loaded=True) - else: - item_data = json.loads(basket_item["item_data"]) - if item_data: - return pd.Series(item_data) - return None - - - def validate_basket_item( - self, basket_item: Union[dict, pd.Series], return_loaded: bool = False - ) -> Union[dict, bool, None]: - """Check that the data in the shopping item conforms with - the expected format - - Parameters - ---------- - basket_item : Union[dict, pd.Series] - A single item from a retrieved shopping basket - either a raw `dict` - or a converted `pd.Series`. - return_loaded : bool - If `True`, and validation succeeds return the extracted shopping item - as `dict`, otherwise return `True` if validation succeeds and `None` - otherwise. - - Returns - ------- - Union[dict, bool, None] - If `return_loaded` is `True`, return a `dict` containing the data - encoded in the shopping item when validation succeeds. - Otherwise if `return_loaded` is `True` validation succeeds. - If validation fails return `None`. - - """ - item_data = json.loads(basket_item["item_data"]) - if "archive" in item_data and item_data["archive"] == self.archive: - if return_loaded: - return item_data - else: - return True - return None \ No newline at end of file diff --git a/astron_vo/__init__.py b/astron_vo/__init__.py deleted file mode 100644 index 00582d00bc41172f3df8b9100833568fe577f0e5..0000000000000000000000000000000000000000 --- a/astron_vo/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from astron_vo.astron_vo_connector import astron_vo_connector diff --git a/esap_client/__init__.py b/esap_client/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d8c3a14b4025922bd18b5b84e3d021e4999e6904 --- /dev/null +++ b/esap_client/__init__.py @@ -0,0 +1 @@ +from .shopping_client import ShoppingClient diff --git a/esap_client/connectors/__init__.py b/esap_client/connectors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..96cc6c913bf722cd778307e86e9fa506fc5319f8 --- /dev/null +++ b/esap_client/connectors/__init__.py @@ -0,0 +1,5 @@ +from .alta import Alta +from .astron_vo import AstronVo +from .samp import Samp +from .zooniverse import Zooniverse +from .rucio import Rucio diff --git a/esap_client/connectors/alta.py b/esap_client/connectors/alta.py new file mode 100644 index 0000000000000000000000000000000000000000..9016aafbd3c1b7cce086c93963fca6e628419112 --- /dev/null +++ b/esap_client/connectors/alta.py @@ -0,0 +1,5 @@ +from .baseConnector import BaseConnector + +class Alta(BaseConnector): + name = "alta" + archive = "apertif" diff --git a/esap_client/connectors/astron_vo.py b/esap_client/connectors/astron_vo.py new file mode 100644 index 0000000000000000000000000000000000000000..de1500ea8ad066c131c652c873b389921ea7b617 --- /dev/null +++ b/esap_client/connectors/astron_vo.py @@ -0,0 +1,5 @@ +from .baseConnector import BaseConnector + +class AstronVo(BaseConnector): + name = "astron_vo" + archive = "astron_vo" diff --git a/astron_vo/astron_vo_connector.py b/esap_client/connectors/baseConnector.py similarity index 96% rename from astron_vo/astron_vo_connector.py rename to esap_client/connectors/baseConnector.py index 32f848aba45ebcab657185166acb12a2aeb3c824..0bb2086f2fedfd2b171ecba2db36167e728d9f75 100644 --- a/astron_vo/astron_vo_connector.py +++ b/esap_client/connectors/baseConnector.py @@ -1,14 +1,9 @@ import json - import pandas as pd from typing import Union, Optional -class astron_vo_connector: - - name = "astron_vo" - archive = "astron_vo" - +class BaseConnector: def basket_item_to_pandas( self, basket_item: Union[dict, pd.Series], validate: bool = True ) -> Optional[pd.Series]: @@ -71,4 +66,4 @@ class astron_vo_connector: return item_data else: return True - return None \ No newline at end of file + return None diff --git a/esap_client/connectors/rucio.py b/esap_client/connectors/rucio.py new file mode 100644 index 0000000000000000000000000000000000000000..a364ebb8c9f6a4ef1d906ae23729dc03e0d2f9a7 --- /dev/null +++ b/esap_client/connectors/rucio.py @@ -0,0 +1,5 @@ +from .baseConnector import BaseConnector + +class Rucio(BaseConnector): + name = "rucio" + archive = "rucio" diff --git a/esap_client/connectors/samp.py b/esap_client/connectors/samp.py new file mode 100644 index 0000000000000000000000000000000000000000..0ac3801ec58f5a06918422fb6036956590c28a09 --- /dev/null +++ b/esap_client/connectors/samp.py @@ -0,0 +1,5 @@ +from .baseConnector import BaseConnector + +class Samp(BaseConnector): + name = "samp" + archive = "samp" diff --git a/zooniverse/zooniverse.py b/esap_client/connectors/zooniverse.py similarity index 75% rename from zooniverse/zooniverse.py rename to esap_client/connectors/zooniverse.py index 91210e52d300ea4ba8fc8980e71c84c683d09143..4eb668a15861e870a8455deae22808b0abc86da7 100644 --- a/zooniverse/zooniverse.py +++ b/esap_client/connectors/zooniverse.py @@ -10,8 +10,9 @@ from warnings import warn from panoptes_client import Panoptes, Project, Workflow from panoptes_client.panoptes import PanoptesAPIException +from .baseConnector import BaseConnector -class zooniverse: +class Zooniverse(BaseConnector): name = "zooniverse" archive = "zooniverse" @@ -155,7 +156,7 @@ class zooniverse: if chunked_retrieve else pd.read_csv( io.BytesIO(response.content), - converters=zooniverse.category_converters[ + converters=Zooniverse.category_converters[ self._get_item_entry(item, "category") ], ) @@ -192,7 +193,7 @@ class zooniverse: chunk_frames.append( pd.read_csv( io.BytesIO(chunk), - converters=zooniverse.category_converters[ + converters=Zooniverse.category_converters[ self._get_item_entry(item, "category") ], header=None if len(chunk_frames) else 0, @@ -212,7 +213,7 @@ class zooniverse: ) def _get_entity(self, item): - entity = zooniverse.entity_types[self._get_item_entry(item, "catalog")].find( + entity = Zooniverse.entity_types[self._get_item_entry(item, "catalog")].find( int(self._get_item_entry(item, self._catalogue_to_id_string(item))) ) return entity @@ -225,66 +226,3 @@ class zooniverse: def _catalogue_to_id_string(self, item): return self._get_item_entry(item, "catalog") + "_id" - - def basket_item_to_pandas( - self, basket_item: Union[dict, pd.Series], validate: bool = True - ) -> Optional[pd.Series]: - """Convert an item from the shopping basket into a `pd.Series` with - optional validation. - - Parameters - ---------- - basket_item : Union[dict, pd.Series] - A single item from a retrieved shopping basket - either a raw `dict` - or a converted `pd.Series`. - validate : bool - If `True`, check that the data in the shopping item conforms with - the expected format before attempting the conversion. - - Returns - ------- - Optional[pd.Series] - `pd.Series` containing the data encoded in the shopping item or - `NoneType`. - - """ - if validate: - item_data = self.validate_basket_item(basket_item, return_loaded=True) - else: - item_data = json.loads(basket_item["item_data"]) - if item_data: - return pd.Series(item_data) - return None - - def validate_basket_item( - self, basket_item: Union[dict, pd.Series], return_loaded: bool = False - ) -> Union[dict, bool, None]: - """Check that the data in the shopping item conforms with - the expected format - - Parameters - ---------- - basket_item : Union[dict, pd.Series] - A single item from a retrieved shopping basket - either a raw `dict` - or a converted `pd.Series`. - return_loaded : bool - If `True`, and validation succeeds return the extracted shopping item - as `dict`, otherwise return `True` if validation succeeds and `None` - otherwise. - - Returns - ------- - Union[dict, bool, None] - If `return_loaded` is `True`, return a `dict` containing the data - encoded in the shopping item when validation succeeds. - Otherwise if `return_loaded` is `True` validation succeeds. - If validation fails return `None`. - - """ - item_data = json.loads(basket_item["item_data"]) - if "archive" in item_data and item_data["archive"] == "zooniverse": - if return_loaded: - return item_data - else: - return True - return None diff --git a/shopping_client/shopping_client.py b/esap_client/shopping_client.py similarity index 98% rename from shopping_client/shopping_client.py rename to esap_client/shopping_client.py index 476284f178e214aaec18c0d156441262103baf3d..9dfe187f01f1959e572cd67e4e174fc981f5c36a 100644 --- a/shopping_client/shopping_client.py +++ b/esap_client/shopping_client.py @@ -14,7 +14,7 @@ import requests logger = logging.getLogger(__name__) -class shopping_client: +class ShoppingClient: endpoint = "esap-api/accounts/user-profiles/" audience = "rucio" # Audience used by ESAP, might be configurable later @@ -80,7 +80,7 @@ class shopping_client: """ if self.basket is None or reload: - url = urllib.parse.urljoin(self.host, shopping_client.endpoint) + url = urllib.parse.urljoin(self.host, ShoppingClient.endpoint) response = requests.get(url, headers=self._request_header()) if response.ok: self.basket = json.loads(response.content)["results"][0][ diff --git a/alta/alta_example.py b/example.py similarity index 74% rename from alta/alta_example.py rename to example.py index 8a35f671a28637ee49ef8acfc3c0d4f463b65910..82b4597d9f8d4039f5c51dff45c3ad03043c3104 100644 --- a/alta/alta_example.py +++ b/example.py @@ -1,9 +1,9 @@ -from shopping_client import shopping_client -from alta import alta_connector -from astron_vo import astron_vo_connector -from samp import samp_connector +from esap_client import ShoppingClient +from esap_client.connectors import Alta as alta_connector +from esap_client.connectors import AstronVo as astron_vo_connector +from esap_client.connectors import Samp as samp_connector -esap_api_host = "https://sdc-dev.astron.nl:5555/" +esap_api_host = "https://sdc-dev.astron.nl:443/" #access_token = "" # Instantiate alta connector @@ -12,7 +12,7 @@ vo = astron_vo_connector() # Instantiate ESAP User Profile shopping client, passing alta connector #sc = shopping_client(host=esap_api_host, token=access_token, connectors=[ac,vo]) -sc = shopping_client(host=esap_api_host, connectors=[ac]) +sc = ShoppingClient(host=esap_api_host, connectors=[ac]) # 'apertif'and 'astron_vo' items converted to pandas dataframe basket_pandas=sc.get_basket(filter_archives=True, convert_to_pandas=True) @@ -29,7 +29,7 @@ print("'apertif'and 'astron_vo' items as json") print(basket_json) samp_connector = samp_connector() -sc = shopping_client(host=esap_api_host, token=access_token, connectors=[samp_connector]) +sc = ShoppingClient(host=esap_api_host, connectors=[samp_connector]) # "'SAMP' items converted to pandas dataframe:" basket_pandas=sc.get_basket(convert_to_pandas=True, filter_archives=True) @@ -42,4 +42,4 @@ print(basket_pandas) basket_json=sc.get_basket(convert_to_pandas=False, filter_archives=True) print('------------------------------------') print("'SAMP' items as json:") -print(basket_json) \ No newline at end of file +print(basket_json) diff --git a/rucio_cli/__init__.py b/rucio_cli/__init__.py deleted file mode 100644 index 720a2b25c7e3faabf7c130fa77dccaee9ee8398f..0000000000000000000000000000000000000000 --- a/rucio_cli/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from rucio_cli.rucio_connector import rucio_connector diff --git a/rucio_cli/rucio_connector.py b/rucio_cli/rucio_connector.py deleted file mode 100644 index 3c98e9cf6197e952df9a3a9faf417a4877888dc2..0000000000000000000000000000000000000000 --- a/rucio_cli/rucio_connector.py +++ /dev/null @@ -1,76 +0,0 @@ -import requests -import json -import io -import getpass -import pandas as pd - -from typing import Union, Optional - -class rucio_connector: - - name = "rucio" - archive = "rucio" - - def basket_item_to_pandas( - self, basket_item: Union[dict, pd.Series], validate: bool = True - ) -> Optional[pd.Series]: - """Convert an item from the shopping basket into a `pd.Series` with - optional validation. - - Parameters - ---------- - basket_item : Union[dict, pd.Series] - A single item from a retrieved shopping basket - either a raw `dict` - or a converted `pd.Series`. - validate : bool - If `True`, check that the data in the shopping item conforms with - the expected format before attempting the conversion. - - Returns - ------- - Optional[pd.Series] - `pd.Series` containing the data encoded in the shopping item or - `NoneType`. - - """ - if validate: - item_data = self.validate_basket_item(basket_item, return_loaded=True) - else: - item_data = json.loads(basket_item["item_data"]) - if item_data: - return pd.Series(item_data) - return None - - - def validate_basket_item( - self, basket_item: Union[dict, pd.Series], return_loaded: bool = False - ) -> Union[dict, bool, None]: - """Check that the data in the shopping item conforms with - the expected format - - Parameters - ---------- - basket_item : Union[dict, pd.Series] - A single item from a retrieved shopping basket - either a raw `dict` - or a converted `pd.Series`. - return_loaded : bool - If `True`, and validation succeeds return the extracted shopping item - as `dict`, otherwise return `True` if validation succeeds and `None` - otherwise. - - Returns - ------- - Union[dict, bool, None] - If `return_loaded` is `True`, return a `dict` containing the data - encoded in the shopping item when validation succeeds. - Otherwise if `return_loaded` is `True` validation succeeds. - If validation fails return `None`. - - """ - item_data = json.loads(basket_item["item_data"]) - if "archive" in item_data and item_data["archive"] == self.archive: - if return_loaded: - return item_data - else: - return True - return None diff --git a/samp/__init__.py b/samp/__init__.py deleted file mode 100644 index e7c8867ecd12e732119be64681b52abb6474dae1..0000000000000000000000000000000000000000 --- a/samp/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from samp.samp_connector import samp_connector \ No newline at end of file diff --git a/samp/samp_connector.py b/samp/samp_connector.py deleted file mode 100644 index 5035b6e03b06aa98504052ff8a29eba2890ad1e6..0000000000000000000000000000000000000000 --- a/samp/samp_connector.py +++ /dev/null @@ -1,76 +0,0 @@ -import requests -import json -import io -import getpass -import pandas as pd - -from typing import Union, Optional - -class samp_connector: - - name = "samp" - archive = "samp" - - def basket_item_to_pandas( - self, basket_item: Union[dict, pd.Series], validate: bool = True - ) -> Optional[pd.Series]: - """Convert an item from the shopping basket into a `pd.Series` with - optional validation. - - Parameters - ---------- - basket_item : Union[dict, pd.Series] - A single item from a retrieved shopping basket - either a raw `dict` - or a converted `pd.Series`. - validate : bool - If `True`, check that the data in the shopping item conforms with - the expected format before attempting the conversion. - - Returns - ------- - Optional[pd.Series] - `pd.Series` containing the data encoded in the shopping item or - `NoneType`. - - """ - if validate: - item_data = self.validate_basket_item(basket_item, return_loaded=True) - else: - item_data = json.loads(basket_item["item_data"]) - if item_data: - return pd.Series(item_data) - return None - - - def validate_basket_item( - self, basket_item: Union[dict, pd.Series], return_loaded: bool = False - ) -> Union[dict, bool, None]: - """Check that the data in the shopping item conforms with - the expected format - - Parameters - ---------- - basket_item : Union[dict, pd.Series] - A single item from a retrieved shopping basket - either a raw `dict` - or a converted `pd.Series`. - return_loaded : bool - If `True`, and validation succeeds return the extracted shopping item - as `dict`, otherwise return `True` if validation succeeds and `None` - otherwise. - - Returns - ------- - Union[dict, bool, None] - If `return_loaded` is `True`, return a `dict` containing the data - encoded in the shopping item when validation succeeds. - Otherwise if `return_loaded` is `True` validation succeeds. - If validation fails return `None`. - - """ - item_data = json.loads(basket_item["item_data"]) - if "archive" in item_data and item_data["archive"] == self.archive: - if return_loaded: - return item_data - else: - return True - return None \ No newline at end of file diff --git a/setup.py b/setup.py index 6393a34de5b3ec9d02647d554a7b959b16c7c30c..0b992333b5a383ca6867d287258a3812cce28298 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,8 @@ with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( - name="esap-userprofile-python-client", - version="0.0.4", + name="esap_client", + version="0.0.5", author="Hugh Dickinson", author_email="hugh.dickinson@open.ac.uk", description="Python client for ESAP Data Discovery Shopping Basket", diff --git a/shopping_client/__init__.py b/shopping_client/__init__.py deleted file mode 100644 index 3e4b0350fbf1e30623f58ae72c4efe8d64144977..0000000000000000000000000000000000000000 --- a/shopping_client/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .shopping_client import shopping_client diff --git a/zooniverse/__init__.py b/zooniverse/__init__.py deleted file mode 100644 index 1d4ed23a78c81d6417dacd0f2c6919c8c6b05184..0000000000000000000000000000000000000000 --- a/zooniverse/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .zooniverse import zooniverse