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