From a5143cec1275fffcb3c4f6b83644113f3e94d3ae Mon Sep 17 00:00:00 2001
From: vermeul <swen@ethz.ch>
Date: Fri, 5 Aug 2022 16:55:38 +0200
Subject: [PATCH] feat: implement personal access token methods

---
 pybis/src/python/pybis/attribute.py   |   2 +-
 pybis/src/python/pybis/definitions.py |  11 ++
 pybis/src/python/pybis/pybis.py       | 215 +++++++++++++++++++-------
 3 files changed, 167 insertions(+), 61 deletions(-)

diff --git a/pybis/src/python/pybis/attribute.py b/pybis/src/python/pybis/attribute.py
index 405b21a3c36..880886870c0 100644
--- a/pybis/src/python/pybis/attribute.py
+++ b/pybis/src/python/pybis/attribute.py
@@ -115,7 +115,7 @@ class AttrHolder:
             elif attr.endswith("Date"):
                 self.__dict__["_" + attr] = format_timestamp(data.get(attr))
 
-            elif attr in ["registrator", "modifier", "dataProducer"]:
+            elif attr in ["registrator", "modifier", "dataProducer", "owner"]:
                 self.__dict__["_" + attr] = extract_person(data.get(attr))
 
             else:
diff --git a/pybis/src/python/pybis/definitions.py b/pybis/src/python/pybis/definitions.py
index 3136f86e3af..3fa68259bbd 100644
--- a/pybis/src/python/pybis/definitions.py
+++ b/pybis/src/python/pybis/definitions.py
@@ -106,6 +106,14 @@ def openbis_definitions(entity):
             "delete": {"@type": "as.dto.dataset.delete.DataSetTypeDeletionOptions"},
             "identifier": "typeId",
         },
+        "personalAccessToken": {
+            "attrs_new": "sessionName validFromDate validToDate accessDate".split(),
+            "attrs_up": "".split(),
+            "attrs": "permId sessionName validFromDate validToDate accessDate owner registrator registrationDate modifier modificationDate".split(),
+            "search": {"@type": "as.dto.pat.search.PersonalAccessTokenSearchCriteria"},
+            "delete": {"@type": "as.dto.pat.delete.PersonalAccessTokenDeletionOptions"},
+            "identifier": "permId",
+        },
         "experimentType": {
             "attrs_new": "code description validationPlugin".split(),
             "attrs_up": "description modificationDate validationPlugin".split(),
@@ -296,6 +304,9 @@ get_definition_for_entity = openbis_definitions  # Alias
 
 
 fetch_option = {
+    "personalAccessToken": {
+        "@type": "as.dto.pat.fetchoptions.PersonalAccessTokenFetchOptions"
+    },
     "space": {"@type": "as.dto.space.fetchoptions.SpaceFetchOptions"},
     "project": {
         "@type": "as.dto.project.fetchoptions.ProjectFetchOptions",
diff --git a/pybis/src/python/pybis/pybis.py b/pybis/src/python/pybis/pybis.py
index b9279007302..6f0c6a1314d 100644
--- a/pybis/src/python/pybis/pybis.py
+++ b/pybis/src/python/pybis/pybis.py
@@ -11,18 +11,17 @@ Work with openBIS using Python.
 from __future__ import print_function
 
 import copy
-import errno
 import json
-import logging
 import os
-import random
 import re
 import subprocess
 import sys
 import time
+from xml.dom import NotSupportedErr
 import zlib
 from collections import defaultdict, namedtuple
 from datetime import datetime
+from dateutil.relativedelta import relativedelta
 from urllib.parse import quote, urljoin, urlparse
 
 import pandas as pd
@@ -31,6 +30,7 @@ import urllib3
 from pandas import DataFrame, Series
 from tabulate import tabulate
 from texttable import Texttable
+from typing import Optional
 
 from . import data_set as pbds
 from .dataset import DataSet
@@ -116,6 +116,7 @@ def get_search_type_for_entity(entity, operator=None):
         {'@type': 'as.dto.space.search.SpaceSearchCriteria'}
     """
     search_criteria = {
+        "personalAccessToken": "as.dto.pat.search.PersonalAccessTokenSearchCriteria",
         "space": "as.dto.space.search.SpaceSearchCriteria",
         "userId": "as.dto.person.search.UserIdSearchCriteria",
         "email": "as.dto.person.search.EmailSearchCriteria",
@@ -169,6 +170,8 @@ def _type_for_id(ident, entity):
             return {"permId": ident, "@type": "as.dto.tag.id.TagPermId"}
         else:
             return {"code": ident, "@type": "as.dto.tag.id.TagCode"}
+    if entity == "personalAccessToken":
+        return {"permId": ident, "@type": "as.dto.pat.id.PersonalAccessTokenPermId"}
 
     entities = {
         "sample": "Sample",
@@ -938,6 +941,7 @@ class Openbis:
         self.use_cache = use_cache
         self.cache = {}
         self.server_information = None
+        self.token = None
         if (
             token is not None
         ):  # We try to set the token, during initialisation instead of errors, a message is printed
@@ -1016,6 +1020,8 @@ class Openbis:
             "get_object_types()",
             "get_property_types()",
             "get_property_type()",
+            "get_personal_access_tokens()",
+            "get_personal_access_token()",
             "new_property_type()",
             "get_semantic_annotations()",
             "get_semantic_annotation()",
@@ -1054,6 +1060,7 @@ class Openbis:
             "new_material_type()",
             "new_semantic_annotation()",
             "new_transaction()",
+            "create_personal_access_token()",
             "set_token()",
         ]
 
@@ -1886,6 +1893,143 @@ class Openbis:
             df_initializer=create_data_frame,
         )
 
+    def create_personal_access_token(
+        self, sessionName: str, validFrom: datetime = None, validTo: datetime = None
+    ) -> str:
+        """Creates a new personal access token (PAT)"""
+
+        if validFrom is None:
+            validFrom = datetime.now()
+        if validTo is None:
+            validTo = datetime.now() + relativedelta(years=1)
+
+        entity = "personalAccessToken"
+        request = {
+            "method": get_method_for_entity(entity, "create"),
+            "params": [
+                self.token,
+                {
+                    "@type": "as.dto.pat.create.PersonalAccessTokenCreation",
+                    "sessionName": sessionName,
+                    "validFromDate": int(validFrom.timestamp() * 1000),
+                    "validToDate": int(validTo.timestamp() * 1000),
+                },
+            ],
+        }
+        try:
+            resp = self._post_request(self.as_v3, request)
+        except ValueError as exc:
+            raise NotSupportedErr(
+                "Your openBIS instance does not support personal access tokens. Please upgrade your server and activate them."
+            )
+        try:
+            token = resp[0]["permId"]
+            return token
+        except KeyError:
+            pass
+            # if "error" in resp and resp["error"]["message"] == "method not found":
+
+    def get_personal_access_tokens(
+        self, permId=None, start_with=None, count=None, **search_args
+    ):
+        """Get Personal Access Tokens"""
+        entity = "personalAccessToken"
+
+        search_criteria = get_search_criteria(entity, **search_args)
+        if permId:
+            sub_crit = _subcriteria_for_permid(permids=permId, entity=entity)
+            search_criteria["criteria"].append(sub_crit)
+        fetchopts = get_fetchoption_for_entity(entity)
+        fetchopts["from"] = start_with
+        fetchopts["count"] = count
+
+        for person in ["owner", "registrator", "modifier"]:
+            fetchopts[person] = get_fetchoption_for_entity(person)
+        request = {
+            "method": get_method_for_entity(entity, "search"),
+            "params": [self.token, search_criteria, fetchopts],
+        }
+        try:
+            resp = self._post_request(self.as_v3, request)
+        except ValueError:
+            raise NotSupportedErr(
+                "This method is not supported by your openBIS instance."
+            )
+
+        defs = get_definition_for_entity(entity)
+
+        def create_data_frame(attrs, props, response):
+            attrs = defs["attrs"]
+            objects = response["objects"]
+            if len(objects) == 0:
+                persons = DataFrame(columns=attrs)
+            else:
+                parse_jackson(objects)
+
+                pats = DataFrame(objects)
+                pats["permId"] = pats["permId"].map(extract_permid)
+                for date in [
+                    "validFromDate",
+                    "validToDate",
+                    "accessDate",
+                    "registrationDate",
+                    "modificationDate",
+                ]:
+                    pats[date] = pats[date].map(format_timestamp)
+                for person in ["owner", "registrator", "modifier"]:
+                    pats[person] = pats[person].map(extract_person)
+            return pats[attrs]
+
+        return Things(
+            openbis_obj=self,
+            entity=entity,
+            identifier_name="permId",
+            single_item_method=self.get_personal_access_token,
+            start_with=start_with,
+            count=count,
+            totalCount=resp.get("totalCount"),
+            response=resp,
+            df_initializer=create_data_frame,
+        )
+
+    def get_personal_access_token(self, permId, only_data=False):
+        entity = "personalAccessToken"
+        identifiers = []
+        only_one = True
+        if isinstance(permId, list):
+            only_one = False
+            for ident in permId:
+                identifiers.append(_type_for_id(ident, entity))
+        else:
+            identifiers.append(_type_for_id(permId, entity))
+
+        defs = get_definition_for_entity(entity)
+        fetchopts = get_fetchoption_for_entity(entity)
+        for person in ["owner", "registrator", "modifier"]:
+            fetchopts[person] = get_fetchoption_for_entity(person)
+        request = {
+            "method": get_method_for_entity(entity, "get"),
+            "params": [self.token, identifiers, fetchopts],
+        }
+        resp = self._post_request(self.as_v3, request)
+        if only_one:
+            if len(resp) == 0:
+                raise ValueError(f"no such {entity} found: {permId}")
+
+            parse_jackson(resp)
+            for permId in resp:
+                if only_data:
+                    return resp[permId]
+                else:
+                    return PersonalAccessToken(
+                        openbis_obj=self,
+                        data=resp[permId],
+                        # single_item_method_name=self.get_personal_access_token,
+                        # count=len(resp),
+                        # totalCount=len(resp),
+                        # response=resp,
+                    )
+
     def get_persons(self, start_with=None, count=None, **search_args):
         """Get openBIS users"""
 
@@ -2107,10 +2251,6 @@ class Openbis:
                         b) property is not defined for this sampleType
         """
 
-        logger = logging.getLogger("get_samples")
-        logger.setLevel(logging.CRITICAL)
-        logger.addHandler(logging.StreamHandler(sys.stdout))
-
         if collection is not None:
             experiment = collection
         if attrs is None:
@@ -2200,23 +2340,11 @@ class Openbis:
             ],
         }
 
-        time1 = now()
-        logger.debug("get_samples posting request")
         resp = self._post_request(self.as_v3, request)
 
-        time2 = now()
-
-        logger.debug(f"get_samples got response. Delay: {time2 - time1}")
         parse_jackson(resp)
 
-        time3 = now()
-
         response = resp["objects"]
-        logger.debug(f"get_samples got JSON. Delay: {time3 - time2}")
-
-        time4 = now()
-
-        logger.debug(f"get_samples after result mapping. Delay: {time4 - time3}")
 
         result = self._sample_list_for_response(
             response=response,
@@ -2228,9 +2356,6 @@ class Openbis:
             parsed=True,
         )
 
-        time5 = now()
-
-        logger.debug(f"get_samples computed final result. Delay: {time5 - time4}")
         return result
 
     get_objects = get_samples  # Alias
@@ -4420,24 +4545,9 @@ class Openbis:
         totalCount=0,
         parsed=False,
     ):
-        logger = logging.getLogger("_sample_list_for_response")
-        logger.setLevel(logging.CRITICAL)
-        logger.disabled = True
-        logger.addHandler(logging.StreamHandler(sys.stdout))
-
-        time1 = now()
-
-        logger.debug("_sample_list_for_response before parsing JSON")
         if not parsed:
             parse_jackson(response)
 
-        time2 = now()
-
-        logger.debug(f"_sample_list_for_response got response. Delay: {time2 - time1}")
-
-        time6 = now()
-        logger.debug("_sample_list_for_response computing result.")
-
         def create_data_frame(attrs, props, response):
             """returns a Things object, containing a DataFrame plus additional information"""
 
@@ -4449,12 +4559,6 @@ class Openbis:
 
                 return return_attribute
 
-            logger = logging.getLogger("create_data_frame")
-            logger.setLevel(logging.CRITICAL)
-            logger.addHandler(logging.StreamHandler(sys.stdout))
-
-            time2 = now()
-
             if attrs is None:
                 attrs = []
             default_attrs = [
@@ -4479,10 +4583,6 @@ class Openbis:
                     display_attrs.append(prop)
                 samples = DataFrame(columns=display_attrs)
             else:
-                time3 = now()
-                logger.debug(
-                    f"createDataFrame computing attributes. Delay: {time3 - time2}"
-                )
 
                 samples = DataFrame(response)
                 for attr in attrs:
@@ -4515,11 +4615,6 @@ class Openbis:
                 samples["permId"] = samples["permId"].map(extract_permid)
                 samples["type"] = samples["type"].map(extract_nested_permid)
 
-                time4 = now()
-                logger.debug(
-                    f"_sample_list_for_response computed attributes. Delay: {time4 - time3}"
-                )
-
                 for prop in props:
                     if prop == "*":
                         # include all properties in dataFrame.
@@ -4545,10 +4640,6 @@ class Openbis:
                                 samples.loc[i, prop.upper()] = ""
                         display_attrs.append(prop.upper())
 
-                time5 = now()
-                logger.debug(
-                    f"_sample_list_for_response computed properties. Delay: {time5 - time4}"
-                )
             return samples[display_attrs]
 
         def create_objects(response):
@@ -4577,10 +4668,6 @@ class Openbis:
             props=props,
         )
 
-        time7 = now()
-        logger.debug(
-            f"_sample_list_for_response computed result. Delay: {time7 - time6}"
-        )
         return result
 
     @staticmethod
@@ -5144,3 +5231,11 @@ class PropertyType(
 
 class Plugin(OpenBisObject, entity="plugin", single_item_method_name="get_plugin"):
     pass
+
+
+class PersonalAccessToken(
+    OpenBisObject,
+    entity="personalAccessToken",
+    single_item_method_name="get_personal_access_token",
+):
+    pass
-- 
GitLab