From f0f765c1815873e47478d105a5cd1c4239004598 Mon Sep 17 00:00:00 2001
From: ahmedk <karim.ahmed@xfel.eu>
Date: Sun, 1 Dec 2024 22:56:51 +0100
Subject: [PATCH] feat: Move post methods into an indvidual class in constants

---
 ...rk_analysis_all_gains_burst_mode_NBC.ipynb |   6 +-
 src/cal_tools/calcat_interface2.py            |  21 +++
 src/cal_tools/constants.py                    | 146 ++++++++++++------
 src/cal_tools/restful_config.py               |  36 +++--
 4 files changed, 144 insertions(+), 65 deletions(-)

diff --git a/notebooks/Jungfrau/Jungfrau_dark_analysis_all_gains_burst_mode_NBC.ipynb b/notebooks/Jungfrau/Jungfrau_dark_analysis_all_gains_burst_mode_NBC.ipynb
index 17d7c74f4..f6a0e9322 100644
--- a/notebooks/Jungfrau/Jungfrau_dark_analysis_all_gains_burst_mode_NBC.ipynb
+++ b/notebooks/Jungfrau/Jungfrau_dark_analysis_all_gains_burst_mode_NBC.ipynb
@@ -92,7 +92,6 @@
     "from cal_tools import step_timing\n",
     "from cal_tools.calcat_interface2 import (\n",
     "    CalibrationData,\n",
-    "    CCVAlreadyInjectedError,\n",
     "    JUNGFRAUConditions,\n",
     ")\n",
     "from cal_tools.constants import (\n",
@@ -613,7 +612,6 @@
     "    constants['Noise10Hz'] = np.moveaxis(noise_map[mod], 0, 1)\n",
     "    constants['BadPixelsDark10Hz'] = np.moveaxis(bad_pixels_map[mod], 0, 1)\n",
     "\n",
-    "    md = None\n",
     "    for const_name, const_data in constants.items():\n",
     "        with NamedTemporaryFile(dir=out_folder) as tempf:\n",
     "            ccv_root = write_ccv(\n",
@@ -657,7 +655,7 @@
     "    f\"• Exposure timeout: {exposure_timeout}\\n\"\n",
     "    f\"• Gain setting: {gain_setting}\\n\"\n",
     "    f\"• Gain mode: {gain_mode}\\n\"\n",
-    "    f\"• Creation time: {md.calibration_constant_version.begin_at if md is not None else creation_time}\\n\")  # noqa\n",
+    "    f\"• Creation time: {creation_time}\\n\")  # noqa\n",
     "step_timer.done_step(\"Injecting constants.\")"
    ]
   },
@@ -696,7 +694,7 @@
     "jf_caldata = CalibrationData.from_condition(\n",
     "    conditions,\n",
     "    karabo_id,\n",
-    "    event_at=creation_time-timedelta(seconds=60) if creation_time else None,\n",
+    "    event_at=creation_time-timedelta(seconds=60),\n",
     "    begin_at_strategy=\"prior\",\n",
     ")\n",
     "\n",
diff --git a/src/cal_tools/calcat_interface2.py b/src/cal_tools/calcat_interface2.py
index 53b01f843..dd159e011 100644
--- a/src/cal_tools/calcat_interface2.py
+++ b/src/cal_tools/calcat_interface2.py
@@ -29,6 +29,27 @@ class ModuleNameError(KeyError):
 
 class CalCatAPIError(requests.HTTPError):
     """Used when the response includes error details as JSON"""
+    ...
+
+class CalibrationConstantNotFound(CalCatAPIError):
+    ...
+
+
+def _get_failed_response(resp):
+    # TODO: Add more errors if needed
+    if "calibration_constant" in resp.url.lstrip("/"):
+        if resp.status_code == 404:
+            raise CalibrationNotFound("Calibration Constant was not found in CALCAT.")
+    if resp.status_code >= 400:
+        try:
+            d = json.loads(resp.content.decode("utf-8"))
+        except Exception:
+            resp.raise_for_status()
+        else:
+            raise CalCatAPIError(
+                f"Error {resp.status_code} from API: "
+                f"{d.get('info', 'missing details')}"
+            )
 
 class CalCatAPIClient:
     def __init__(self, base_api_url, oauth_client=None, user_email=""):
diff --git a/src/cal_tools/constants.py b/src/cal_tools/constants.py
index b7d4f8fc9..e62940280 100644
--- a/src/cal_tools/constants.py
+++ b/src/cal_tools/constants.py
@@ -19,7 +19,9 @@ from cal_tools.calcat_interface2 import (
     CalCatAPIClient,
     CalCatAPIError,
     get_default_caldb_root,
+    CalibrationConstantNotFound
 )
+from oauth2_xfel_client import Oauth2ClientBackend
 
 CONDITION_NAME_MAX_LENGTH = 60
 
@@ -32,12 +34,12 @@ class CCVAlreadyInjectedError(InjectAPIError):
     ...
 
 
-def _failed_response(resp):
+def _post_failed_response(resp):
     # TODO: Add more errors if needed
     if "calibration_constant_versions" in resp.url.lstrip("/"):
         if resp.status_code == 422:
             raise CCVAlreadyInjectedError
-    elif resp.status_code >= 400:
+    if resp.status_code >= 400:
         try:
             d = json.loads(resp.content.decode("utf-8"))
         except Exception:
@@ -70,7 +72,7 @@ class InjectAPI(CalCatAPIClient):
     def _parse_post_response(resp: requests.Response):
 
         if resp.status_code >= 400:
-            _failed_response(resp)
+            _post_failed_response(resp)
 
         if resp.content == b"":
             return None
@@ -107,8 +109,29 @@ class ParameterConditionAttribute:
     description: str = ''
 
 
-def generate_unique_cond_name(detector_type, pdu_name, pdu_uuid, cond_params):
-    # Generate condition name.
+def generate_unique_condition_name(
+    detector_type: str,
+    pdu_name: str,
+    pdu_uuid: float,
+    cond_params: List[dict],
+):
+    """Generate a unique condition using UUID and timestamp.
+
+    Args:
+        detector_type (str): detector type.
+        pdu_name (str): Physical detector unit db name.
+        pdu_uuid (float): Physical detector unit db id.
+        cond_params (List[dict]): A list of dictionary with each condition
+            e.g. [{
+                "parameter_name": "Memory Cells",
+                "value": 352.0,
+                "lower-deviation": 0.0,
+                "upper-deviation": 0.0
+            }]
+
+    Returns:
+        str:  A unique name used for the table of conditions.
+    """
     unique_name = detector_type[:detector_type.index('-Type')] + ' Def'
     cond_hash = md5(pdu_name.encode())
     cond_hash.update(int(pdu_uuid).to_bytes(
@@ -119,7 +142,7 @@ def generate_unique_cond_name(detector_type, pdu_name, pdu_uuid, cond_params):
         cond_hash.update(str(pattrs.value).encode())
 
     unique_name += binascii.b2a_base64(cond_hash.digest()).decode()
-    return unique_name[:60]
+    return unique_name[:CONDITION_NAME_MAX_LENGTH]
 
 
 def create_unique_cc_name(det_type, calibration, condition_name):
@@ -270,44 +293,8 @@ def get_condition_dict(
     }
 
 
-def generate_unique_condition_name(
-    detector_type: str,
-    pdu_name: str,
-    pdu_uuid: float,
-    cond_params: List[dict],
-):
-    """Generate a unique condition using UUID and timestamp.
-
-    Args:
-        detector_type (str): detector type.
-        pdu_name (str): Physical detector unit db name.
-        pdu_uuid (float): Physical detector unit db id.
-        cond_params (List[dict]): A list of dictionary with each condition
-            e.g. [{
-                "parameter_name": "Memory Cells",
-                "value": 352.0,
-                "lower-deviation": 0.0,
-                "upper-deviation": 0.0
-            }]
-
-    Returns:
-        str:  A unique name used for the table of conditions.
-    """
-    unique_name = detector_type[:detector_type.index('-Type')] + ' Def'
-    cond_hash = md5(pdu_name.encode())
-    cond_hash.update(int(pdu_uuid).to_bytes(
-        length=8, byteorder='little', signed=False))
-
-    for param_dict in cond_params:
-        cond_hash.update(str(param_dict['parameter_name']).encode())
-        cond_hash.update(str(param_dict['value']).encode())
-
-    unique_name += binascii.b2a_base64(cond_hash.digest()).decode()
-    return unique_name[:CONDITION_NAME_MAX_LENGTH]
-
-
 def get_or_create_calibration_constant(
-    client: CalCatAPIClient,
+    client: InjectAPI,
     calibration: str,
     detector_type: str,
     condition_id: int,
@@ -329,9 +316,11 @@ def get_or_create_calibration_constant(
         flg_available = 'true',
         description = "",
     )
-    resp = client.get_calibration_constant(calibration_constant)
-    return resp['id'] if resp else client.create_calibration_constant(
-        calibration_constant)['id']  # calibration constant id
+    try:
+        cc_id = client.get_calibration_constant(calibration_constant)
+    except CalibrationConstantNotFound:
+        cc_id = client.create_calibration_constant(calibration_constant)['id']
+    return cc_id
 
 
 def create_condition(
@@ -342,7 +331,7 @@ def create_condition(
     cond_params: dict,
     ) -> Tuple[int, str]:
     # Create condition unique name
-    cond_name = generate_unique_cond_name(
+    cond_name = generate_unique_condition_name(
         detector_type, pdu_name, pdu_uuid, cond_params)
 
     # Add the missing parameter_id value in `ParameterConditionAttribute`s.
@@ -398,6 +387,68 @@ def get_ccv_info_from_file(
 
     return cond_params, begin_at, pdu_uuid, detector_type, raw_data_location
 
+global_client = None
+
+
+def get_client():
+    """Get the global CalCat API client.
+
+    The default assumes we're running in the DESY network; this is used unless
+    `setup_client()` has been called to specify otherwise.
+    """
+    global global_client
+    if global_client is None:
+        setup_client(CALCAT_PROXY_URL, None, None, None)
+    return global_client
+
+
+def setup_client(
+    base_url,
+    client_id,
+    client_secret,
+    user_email,
+    scope="",
+    session_token=None,
+    oauth_retries=3,
+    oauth_timeout=12,
+    ssl_verify=True,
+):
+    """Configure the global CalCat API client."""
+    global global_client
+    if client_id is not None:
+        oauth_client = Oauth2ClientBackend(
+            client_id=client_id,
+            client_secret=client_secret,
+            scope=scope,
+            token_url=f"{base_url}/oauth/token",
+            session_token=session_token,
+            max_retries=oauth_retries,
+            timeout=oauth_timeout,
+            ssl_verify=ssl_verify,
+        )
+    else:
+        oauth_client = None
+    global_client = InjectAPI(
+        f"{base_url}/api/",
+        oauth_client=oauth_client,
+        user_email=user_email,
+    )
+
+    # Check we can connect to exflcalproxy
+    if oauth_client is None and base_url == CALCAT_PROXY_URL:
+        try:
+            # timeout=(connect_timeout, read_timeout)
+            global_client.get_request("me", timeout=(1, 5))
+        except requests.ConnectionError as e:
+            raise RuntimeError(
+                "Could not connect to calibration catalog proxy. This proxy allows "
+                "unauthenticated access inside the XFEL/DESY network. To look up "
+                "calibration constants from outside, you will need to create an Oauth "
+                "client ID & secret in the CalCat web interface. You will still not "
+                "be able to load constants without the constant store folder."
+            ) from e
+
+
 
 def inject_ccv(const_src, ccv_root, report_to=None, client=None):
     """Inject new CCV into CalCat.
@@ -410,7 +461,6 @@ def inject_ccv(const_src, ccv_root, report_to=None, client=None):
         RuntimeError: If CalCat POST request fails.
     """
     if client is None:
-        from cal_tools.calcat_interface2 import get_client
         client = get_client()
 
     pdu_name, calibration, _ = ccv_root.lstrip('/').split('/')
diff --git a/src/cal_tools/restful_config.py b/src/cal_tools/restful_config.py
index 671dec619..1e2017dec 100644
--- a/src/cal_tools/restful_config.py
+++ b/src/cal_tools/restful_config.py
@@ -8,7 +8,7 @@ config_dir = Path(__file__).parent.resolve()
 
 # Default fles.
 settings_files = [
-    config_dir / "restful_config.yaml",
+    config_dir / "restful_test_config.yaml",
     config_dir / "restful_config.secrets.yaml",
     Path("~/.config/pycalibration/cal_tools/restful_config.yaml").expanduser(),
 ]
@@ -44,10 +44,10 @@ def calibration_client():
         scope='')
 
 
-def extra_calibration_client():
+def extra_calibration_client(inject=False):
     """Obtain an initialized CalCatAPIClient object."""
 
-    from cal_tools import calcat_interface2
+    from cal_tools import calcat_interface2, constants
 
     calcat_config = restful_config.get('calcat')
     user_id = user_secret = None
@@ -57,13 +57,23 @@ def extra_calibration_client():
     base_api_url = calcat_config['base-api-url'].rstrip('/')
     assert base_api_url.endswith('/api')
     base_url = base_api_url[:-4]
-
-    calcat_interface2.setup_client(
-        base_url,
-        client_id=user_id,
-        client_secret=user_secret,
-        user_email=calcat_config['user-email'],
-    )
-    if calcat_config['caldb-root']:
-        calcat_interface2.set_default_caldb_root(Path(calcat_config['caldb-root']))
-    return calcat_interface2.get_client()
+    if inject:
+        constants.setup_client(
+            base_url,
+            client_id=user_id,
+            client_secret=user_secret,
+            user_email=calcat_config['user-email'],
+        )
+        if calcat_config['caldb-root']:
+            calcat_interface2.set_default_caldb_root(Path(calcat_config['caldb-root']))
+        return constants.get_client()
+    else:
+        calcat_interface2.setup_client(
+            base_url,
+            client_id=user_id,
+            client_secret=user_secret,
+            user_email=calcat_config['user-email'],
+        )
+        if calcat_config['caldb-root']:
+            calcat_interface2.set_default_caldb_root(Path(calcat_config['caldb-root']))
+        return calcat_interface2.get_client()
-- 
GitLab