From 85306c706e7c01a0bf1869c29f5ed8c7a90fec9d Mon Sep 17 00:00:00 2001
From: John Swinbank <swinbank@astron.nl>
Date: Fri, 22 Apr 2022 16:57:08 +0200
Subject: [PATCH] Restructure and rename

This moves all code into the esap_client package to avoid name clashes with
other code (e.g.
https://git.astron.nl/astron-sdc/escape-wp5/esap-general/-/issues/86). It uses
inheritance to reduce duplication. It makes a few small changes to make the
class names idiomatic (ie, using CamelCase).

This is a breaking API change!
---
 README.md                                     | 35 +--------
 __init__.py                                   |  2 -
 alta/__init__.py                              |  1 -
 alta/alta_connector.py                        | 76 -------------------
 astron_vo/__init__.py                         |  1 -
 esap_client/__init__.py                       |  1 +
 esap_client/connectors/__init__.py            |  5 ++
 esap_client/connectors/alta.py                |  5 ++
 esap_client/connectors/astron_vo.py           |  5 ++
 .../connectors/baseConnector.py               |  9 +--
 esap_client/connectors/rucio.py               |  5 ++
 esap_client/connectors/samp.py                |  5 ++
 .../connectors}/zooniverse.py                 | 72 ++----------------
 .../shopping_client.py                        |  4 +-
 alta/alta_example.py => example.py            | 16 ++--
 rucio_cli/__init__.py                         |  1 -
 rucio_cli/rucio_connector.py                  | 76 -------------------
 samp/__init__.py                              |  1 -
 samp/samp_connector.py                        | 76 -------------------
 setup.py                                      |  4 +-
 shopping_client/__init__.py                   |  1 -
 zooniverse/__init__.py                        |  1 -
 22 files changed, 49 insertions(+), 353 deletions(-)
 delete mode 100644 __init__.py
 delete mode 100644 alta/__init__.py
 delete mode 100644 alta/alta_connector.py
 delete mode 100644 astron_vo/__init__.py
 create mode 100644 esap_client/__init__.py
 create mode 100644 esap_client/connectors/__init__.py
 create mode 100644 esap_client/connectors/alta.py
 create mode 100644 esap_client/connectors/astron_vo.py
 rename astron_vo/astron_vo_connector.py => esap_client/connectors/baseConnector.py (96%)
 create mode 100644 esap_client/connectors/rucio.py
 create mode 100644 esap_client/connectors/samp.py
 rename {zooniverse => esap_client/connectors}/zooniverse.py (75%)
 rename {shopping_client => esap_client}/shopping_client.py (98%)
 rename alta/alta_example.py => example.py (74%)
 delete mode 100644 rucio_cli/__init__.py
 delete mode 100644 rucio_cli/rucio_connector.py
 delete mode 100644 samp/__init__.py
 delete mode 100644 samp/samp_connector.py
 delete mode 100644 shopping_client/__init__.py
 delete mode 100644 zooniverse/__init__.py

diff --git a/README.md b/README.md
index 9151987..71caa38 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 adaa6db..0000000
--- 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 93ccfeb..0000000
--- 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 c514c26..0000000
--- 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 00582d0..0000000
--- 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 0000000..d8c3a14
--- /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 0000000..96cc6c9
--- /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 0000000..9016aaf
--- /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 0000000..de1500e
--- /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 32f848a..0bb2086 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 0000000..a364ebb
--- /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 0000000..0ac3801
--- /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 91210e5..4eb668a 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 476284f..9dfe187 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 8a35f67..82b4597 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 720a2b2..0000000
--- 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 3c98e9c..0000000
--- 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 e7c8867..0000000
--- 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 5035b6e..0000000
--- 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 6393a34..0b99233 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 3e4b035..0000000
--- 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 1d4ed23..0000000
--- a/zooniverse/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .zooniverse import zooniverse
-- 
GitLab