Exemple #1
0
    def close_par(par=None, par_uid=None, url_checksum=None):
        """Close the passed OSPar, which provides access to data in the
           passed bucket

           Args:
                par (OSPar, default=None): OSPar to close bucket
                par_uid (str, default=None): UID for OSPar
                url_checksum (str, default=None): Checksum to
                pass to PARRegistry
           Returns:
                None
        """
        from Acquire.ObjectStore import OSParRegistry as _OSParRegistry

        if par is None:
            par = _OSParRegistry.get(
                par_uid=par_uid,
                details_function=_get_driver_details_from_data,
                url_checksum=url_checksum)

        from Acquire.ObjectStore import OSPar as _OSPar
        if not isinstance(par, _OSPar):
            raise TypeError("The OSPar must be of type OSPar")

        if par.driver() != "oci":
            raise ValueError("Cannot delete a OSPar that was not created "
                             "by the OCI object store")

        # delete the PAR
        from Acquire.Service import get_service_account_bucket \
            as _get_service_account_bucket

        par_bucket = par.driver_details()["bucket"]
        par_id = par.driver_details()["par_id"]

        bucket = _get_service_account_bucket()

        # now get the bucket accessed by the OSPar...
        bucket = OCI_ObjectStore.get_bucket(bucket=bucket,
                                            bucket_name=par_bucket)

        client = bucket["client"]

        try:
            response = client.delete_preauthenticated_request(
                client.get_namespace().data, bucket["bucket_name"], par_id)
        except Exception as e:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError("Unable to delete a OSPar '%s' : Error %s" %
                                   (par_id, str(e)))

        if response.status not in [200, 204]:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "Unable to delete a OSPar '%s' : Status %s, Error %s" %
                (par_id, response.status, str(response.data)))

        # close the OSPar - this will trigger any close_function(s)
        _OSParRegistry.close(par=par)
Exemple #2
0
    def delete_bucket(bucket, force=False):
        """Delete the passed bucket. This should be used with caution.
           Normally you can only delete a bucket if it is empty. If
           'force' is True then it will remove all objects/pars from
           the bucket first, and then delete the bucket. This
           can cause a LOSS OF DATA!

           Args:
                bucket (dict): Bucket to delete
                force (bool, default=False): If True, delete even
                if bucket is not empty. If False and bucket not empty
                raise PermissionError
           Returns:
                None
        """
        is_empty = GCP_ObjectStore.is_bucket_empty(bucket=bucket)

        if not is_empty:
            if force:
                GCP_ObjectStore.delete_all_objects(bucket=bucket)
            else:
                raise PermissionError(
                    "You cannot delete the bucket %s as it is not empty" %
                    GCP_ObjectStore.get_bucket_name(bucket=bucket))

        # the bucket is empty - delete it
        try:
            bucket['bucket'].delete()
        except Exception as e:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "Unable to delete bucket '%s'. Please check the "
                "access permissions: Error %s" %
                (bucket['bucket_name'], str(e)))
Exemple #3
0
    def create_bucket(bucket, bucket_name):
        """Create and return a new bucket in the object store called
           'bucket_name'. This will raise an
           ObjectStoreError if this bucket already exists
        """
        new_bucket = _copy.copy(bucket)

        try:
            from google.cloud import storage as _storage
            client = new_bucket["client"]
            bucket_name = _sanitise_bucket_name(bucket_name,
                                                bucket["unique_suffix"])
            bucket_obj = _storage.Bucket(client, name=bucket_name)
            bucket_obj.location = bucket["bucket"].location
            bucket_obj.storage_class = "REGIONAL"
            new_bucket["bucket"] = client.create_bucket(bucket_obj)
            new_bucket["bucket_name"] = str(bucket_name)
        except Exception as e:
            # couldn't create the bucket - likely because it already
            # exists - try to connect to the existing bucket
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "Unable to create the bucket '%s', likely because it "
                "already exists: %s" % (bucket_name, str(e)))

        return new_bucket
    def get_bucket(bucket,
                   bucket_name,
                   compartment=None,
                   create_if_needed=True):
        """Find and return a new bucket in the object store called
           'bucket_name', optionally placing it into the compartment
           identified by 'compartment'. If 'create_if_needed' is True
           then the bucket will be created if it doesn't exist. Otherwise,
           if the bucket does not exist then an exception will be raised.
        """
        bucket_name = str(bucket_name)

        if compartment is not None:
            if compartment.endswith("/"):
                bucket = compartment
            else:
                bucket = "%s/" % compartment

        full_name = _os.path.join(_os.path.split(bucket)[0], bucket_name)

        if not _os.path.exists(full_name):
            if create_if_needed:
                _os.makedirs(full_name)
            else:
                from Acquire.ObjectStore import ObjectStoreError
                raise ObjectStoreError(
                    "There is no bucket available called '%s' in "
                    "compartment '%s'" % (bucket_name, compartment))

        return full_name
    def get_size_and_checksum(bucket, key):
        """Return the object size (in bytes) and MD5 checksum of the
           object in the passed bucket at the specified key

           Args:
                bucket (dict): Bucket containing data
                key (str): Key for object
           Returns:
                tuple (int, str): Size and MD5 checksum of object

        """
        key = _clean_key(key)

        try:
            response = bucket["client"].get_object(bucket["namespace"],
                                                   bucket["bucket_name"],
                                                   key)
        except:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError("No data at key '%s'" % key)

        content_length = response.headers["Content-Length"]
        checksum = response.headers["Content-MD5"]

        # the checksum is a base64 encoded Content-MD5 header
        # described as standard part of HTTP RFC 2616. Need to
        # convert this back to a hexdigest
        import binascii as _binascii
        import base64 as _base64
        md5sum = _binascii.hexlify(_base64.b64decode(checksum)).decode("utf-8")

        return (int(content_length), md5sum)
Exemple #6
0
    def get_bucket(bucket, bucket_name, create_if_needed=True):
        """Find and return a new bucket in the object store called
           'bucket_name'. If 'create_if_needed' is True
           then the bucket will be created if it doesn't exist. Otherwise,
           if the bucket does not exist then an exception will be raised.
        """
        new_bucket = _copy.copy(bucket)

        new_bucket["bucket_name"] = _sanitise_bucket_name(bucket_name)

        try:
            from oci.object_storage.models import CreateBucketDetails as \
                _CreateBucketDetails
        except:
            raise ImportError(
                "Cannot import OCI. Please install OCI, e.g. via "
                "'pip install oci' so that you can connect to the "
                "Oracle Cloud Infrastructure")

        # try to get the existing bucket
        client = new_bucket["client"]
        namespace = client.get_namespace().data
        sanitised_name = _sanitise_bucket_name(bucket_name)

        try:
            existing_bucket = client.get_bucket(namespace, sanitised_name).data
        except:
            existing_bucket = None

        if existing_bucket:
            new_bucket["bucket"] = existing_bucket
            return new_bucket

        if create_if_needed:
            try:
                request = _CreateBucketDetails()
                request.compartment_id = new_bucket["compartment_id"]
                request.name = sanitised_name

                client.create_bucket(namespace, request)
            except:
                pass

            try:
                existing_bucket = client.get_bucket(namespace,
                                                    sanitised_name).data
            except:
                existing_bucket = None

        if existing_bucket is None:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "There is not bucket called '%s'. Please check the "
                "compartment and access permissions." % bucket_name)

        new_bucket["bucket"] = existing_bucket

        return new_bucket
    def delete_bucket(bucket, force=False):
        """Delete the passed bucket. This should be used with caution.
           Normally you can only delete a bucket if it is empty. If
           'force' is True then it will remove all objects/pars from
           the bucket first, and then delete the bucket. This
           can cause a LOSS OF DATA!

           Args:
                bucket (dict): Bucket to delete
                force (bool, default=False): If True, delete even
                if bucket is not empty. If False and bucket not empty
                raise PermissionError
           Returns:
                None
        """
        is_empty = OCI_ObjectStore.is_bucket_empty(bucket=bucket)

        if not is_empty:
            if force:
                OCI_ObjectStore.delete_all_objects(bucket=bucket)
            else:
                raise PermissionError(
                    "You cannot delete the bucket %s as it is not empty" %
                    OCI_ObjectStore.get_bucket_name(bucket=bucket))

        # the bucket is empty - delete it
        client = bucket["client"]
        namespace = client.get_namespace().data
        bucket_name = bucket["bucket_name"]

        try:
            response = client.delete_bucket(namespace, bucket_name)
        except Exception as e:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "Unable to delete bucket '%s'. Please check the "
                "compartment and access permissions: Error %s" %
                (bucket_name, str(e)))

        if response.status not in [200, 204]:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "Unable to delete a bucket '%s' : Status %s, Error %s" %
                (bucket_name, response.status, str(response.data)))
Exemple #8
0
    def get_object(bucket, key):
        """Return the binary data contained in the key 'key' in the
           passed bucket

           Args:
                bucket (dict): Bucket containing data
                key (str): Key for data in bucket
           Returns:
                bytes: Binary data

        """

        key = _clean_key(key)

        blob = bucket["bucket"].blob(key)

        try:
            response = blob.download_as_string()
            is_chunked = False
        except:
            try:
                blob = bucket["bucket"].blob("%s/1" % key)
                response = blob.download_as_string()
                is_chunked = True
            except:
                is_chunked = False
                pass

            # Raise the original error
            if not is_chunked:
                from Acquire.ObjectStore import ObjectStoreError
                raise ObjectStoreError("No data at key '%s'" % key)

        data = response

        if is_chunked:
            # keep going through to find more chunks
            next_chunk = 1

            while True:
                next_chunk += 1

                try:
                    blob = bucket["bucket"].blob("%s/%s" % (key, next_chunk))
                    response = blob.download_as_string()
                except:
                    response = None
                    break

                if not data:
                    data = response
                else:
                    data += response

        return data
    def get_object(bucket, key):
        """Return the binary data contained in the key 'key' in the
           passed bucket"""

        with _rlock:
            filepath = "%s/%s._data" % (bucket, key)
            if _os.path.exists(filepath):
                return open(filepath, "rb").read()
            else:
                from Acquire.ObjectStore import ObjectStoreError
                raise ObjectStoreError("No object at key '%s'" % key)
 def take_object(bucket, key):
     """Take (delete) the object from the object store, returning
        the object
     """
     with _rlock:
         filepath = "%s/%s._data" % (bucket, key)
         if _os.path.exists(filepath):
             data = open(filepath, "rb").read()
             _os.remove(filepath)
             return data
         else:
             from Acquire.ObjectStore import ObjectStoreError
             raise ObjectStoreError("No object at key '%s'" % key)
    def get_size_and_checksum(bucket, key):
        """Return the object size (in bytes) and checksum of the
           object in the passed bucket at the specified key
        """
        filepath = "%s/%s._data" % (bucket, key)

        if not _os.path.exists(filepath):
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError("No object at key '%s'" % key)

        from Acquire.Access import get_filesize_and_checksum \
            as _get_filesize_and_checksum

        return _get_filesize_and_checksum(filepath)
Exemple #12
0
def set_object_store_backend(backend):
    """Set the backend that is used to actually connect to
       the object store. This can only be set once in the program!
    """
    global _objstore_backend

    if backend == _objstore_backend:
        return

    if _objstore_backend is not None:
        from Acquire.ObjectStore import ObjectStoreError
        raise ObjectStoreError("You cannot change the object store "
                               "backend once it has been already set!")

    _objstore_backend = backend
    def create_bucket(bucket, bucket_name, compartment=None):
        """Create and return a new bucket in the object store called
           'bucket_name', optionally placing it into the compartment
           identified by 'compartment'. This will raise an
           ObjectStoreError if this bucket already exists

           Args:
            bucket (dict): Bucket to hold data
            bucket_name (str): Name of bucket to create
            compartment (str): Compartment in which to create bucket

           Returns:
                dict: New bucket
        """
        new_bucket = _copy.copy(bucket)

        new_bucket["bucket_name"] = str(bucket_name)

        if compartment is not None:
            new_bucket["compartment_id"] = str(compartment)

        try:
            from oci.object_storage.models import CreateBucketDetails as \
                _CreateBucketDetails
        except:
            raise ImportError(
                "Cannot import OCI. Please install OCI, e.g. via "
                "'pip install oci' so that you can connect to the "
                "Oracle Cloud Infrastructure")

        try:
            request = _CreateBucketDetails()
            request.compartment_id = new_bucket["compartment_id"]
            client = new_bucket["client"]
            request.name = _sanitise_bucket_name(bucket_name)

            new_bucket["bucket"] = client.create_bucket(
                                        client.get_namespace().data,
                                        request).data
        except Exception as e:
            # couldn't create the bucket - likely because it already
            # exists - try to connect to the existing bucket
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "Unable to create the bucket '%s', likely because it "
                "already exists: %s" % (bucket_name, str(e)))

        return new_bucket
Exemple #14
0
def get_object(bucket, key):
    """ Gets the object at key in the passed bucket

        Args:
            bucket (str): Bucket containing data
            key (str): Key for data in bucket
        Returns:
            Object: Object from store
    """
    with rlock:
        filepath = Path(f"{bucket}/{key}._data")

        if filepath.exists():
            return filepath.read_bytes()
        else:
            raise ObjectStoreError(f"No object at key '{key}'")
Exemple #15
0
    def create_bucket(bucket, bucket_name):
        """Create and return a new bucket in the object store called
           'bucket_name'. This will raise an
           ObjectStoreError if this bucket already exists
        """
        bucket_name = str(bucket_name)

        full_name = _os.path.join(_os.path.split(bucket)[0], bucket_name)

        if _os.path.exists(full_name):
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "CANNOT CREATE NEW BUCKET '%s': EXISTS!" % bucket_name)

        _os.makedirs(full_name)

        return full_name
Exemple #16
0
    def get_bucket(bucket, bucket_name, create_if_needed=True):
        """Find and return a new bucket in the object store called
           'bucket_name'. If 'create_if_needed' is True
           then the bucket will be created if it doesn't exist. Otherwise,
           if the bucket does not exist then an exception will be raised.
        """
        bucket_name = str(bucket_name)

        full_name = _os.path.join(_os.path.split(bucket)[0], bucket_name)

        if not _os.path.exists(full_name):
            if create_if_needed:
                _os.makedirs(full_name)
            else:
                from Acquire.ObjectStore import ObjectStoreError
                raise ObjectStoreError(
                    "There is no bucket available called '%s'"
                    % (bucket_name))

        return full_name
Exemple #17
0
    def get_bucket(bucket, bucket_name, create_if_needed=True):
        """Find and return a new bucket in the object store called
           'bucket_name'. If 'create_if_needed' is True
           then the bucket will be created if it doesn't exist. Otherwise,
           if the bucket does not exist then an exception will be raised.
        """
        new_bucket = _copy.copy(bucket)

        # try to get the existing bucket
        client = new_bucket["client"]
        sanitised_name = _sanitise_bucket_name(bucket_name,
                                               bucket["unique_suffix"])
        new_bucket["bucket_name"] = sanitised_name
        try:
            existing_bucket = client.get_bucket(sanitised_name)
        except:
            existing_bucket = None

        if existing_bucket:
            new_bucket["bucket"] = existing_bucket
            return new_bucket

        if create_if_needed:
            try:
                new_bucket = GCP_ObjectStore.create_bucket(bucket, bucket_name)
                existing_bucket = new_bucket["bucket"]
            except:
                existing_bucket = None

        if existing_bucket is None:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "There is not bucket called '%s'. Please check the "
                "access permissions." % bucket_name)

        new_bucket["bucket"] = existing_bucket

        return new_bucket
def _clean_key(key):
    """This function cleans and returns a key so that it is suitable
       for use both as a key and a directory/file path
       e.g. it removes double-slashes

       Args:
            key (str): Key to clean
       Returns:
            str: Cleaned key

    """
    key = _os.path.normpath(key)

    if len(key) > 1024:
        from Acquire.ObjectStore import ObjectStoreError
        raise ObjectStoreError(
            "The object store does not support keys with longer than "
            "1024 characters (%s) - %s" % (len(key), key))

        # if this becomes a problem then we will implement a 'tinyurl'
        # to shorten keys and use this function to lookup long keys

    return key
    def create_bucket(bucket, bucket_name, compartment=None):
        """Create and return a new bucket in the object store called
           'bucket_name', optionally placing it into the compartment
           identified by 'compartment'. This will raise an
           ObjectStoreError if this bucket already exists
        """
        bucket_name = str(bucket_name)

        if compartment is not None:
            if compartment.endswith("/"):
                bucket = compartment
            else:
                bucket = "%s/" % compartment

        full_name = _os.path.join(_os.path.split(bucket)[0], bucket_name)

        if _os.path.exists(full_name):
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError("CANNOT CREATE NEW BUCKET '%s': EXISTS!" %
                                   bucket_name)

        _os.makedirs(full_name)

        return full_name
Exemple #20
0
    def create_par(bucket,
                   encrypt_key,
                   key=None,
                   readable=True,
                   writeable=False,
                   duration=3600,
                   cleanup_function=None):
        """Create a pre-authenticated request for the passed bucket and
           key (if key is None then the request is for the entire bucket).
           This will return a OSPar object that will contain a URL that can
           be used to access the object/bucket. If writeable is true, then
           the URL will also allow the object/bucket to be written to.
           PARs are time-limited. Set the lifetime in seconds by passing
           in 'duration' (by default this is one hour)

           Args:
                bucket (dict): Bucket to create OSPar for
                encrypt_key (PublicKey): Public key to
                encrypt PAR
                key (str, default=None): Key
                readable (bool, default=True): If bucket is readable
                writeable (bool, default=False): If bucket is writeable
                duration (int, default=3600): Duration OSPar should be
                valid for in seconds
                cleanup_function (function, default=None): Cleanup
                function to be passed to PARRegistry

           Returns:
                OSPar: Pre-authenticated request for the bucket
        """
        from Acquire.Crypto import PublicKey as _PublicKey

        if not isinstance(encrypt_key, _PublicKey):
            from Acquire.Client import PARError
            raise PARError("You must supply a valid PublicKey to encrypt the "
                           "returned OSPar")

        is_bucket = (key is None)

        if writeable:
            method = "PUT"
        elif readable:
            method = "GET"
        else:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError("Unsupported permissions model for OSPar!")

        try:
            # get the UTC datetime when this OSPar should expire
            from Acquire.ObjectStore import get_datetime_now as _get_datetime_now
            created_datetime = _get_datetime_now()
            expires_datetime = _get_datetime_now() + _datetime.timedelta(
                seconds=duration)
            bucket_obj = bucket["bucket"]
            if is_bucket:
                url = bucket_obj.generate_signed_url(
                    version='v4', expiration=expires_datetime, method=method)
            else:
                blob = bucket_obj.blob(key)
                url = blob.generate_signed_url(version='v4',
                                               expiration=expires_datetime,
                                               method=method)

        except Exception as e:
            # couldn't create the preauthenticated request
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError("Unable to create the OSPar '%s': %s" %
                                   (key, str(e)))

        if url is None:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError("Unable to create the signed URL!")

        # get the checksum for this URL - used to validate the close
        # request
        from Acquire.ObjectStore import OSPar as _OSPar
        from Acquire.ObjectStore import OSParRegistry as _OSParRegistry
        url_checksum = _OSPar.checksum(url)
        bucket_name = bucket["bucket_name"]
        driver_details = {
            "driver": "gcp",
            "bucket": bucket_name,
            "created_datetime": created_datetime
        }

        par = _OSPar(url=url,
                     encrypt_key=encrypt_key,
                     key=key,
                     expires_datetime=expires_datetime,
                     is_readable=readable,
                     is_writeable=writeable,
                     driver_details=driver_details)

        _OSParRegistry.register(par=par,
                                url_checksum=url_checksum,
                                details_function=_get_driver_details_from_par,
                                cleanup_function=cleanup_function)

        return par
    def get(par_uid, details_function, url_checksum=None):
        """Return the PAR that matches the passed PAR_UID.
           If 'url_checksum' is supplied then this verifies that
           the checksum of the secret URL is correct.

           This returns the PAR with a completed 'driver_details'.
           The 'driver_details' is created from the dictionary
           of data saved with the PAR. The signature should be;

           driver_details = details_function(data)
        """
        if par_uid is None or len(par_uid) == 0:
            return

        from Acquire.Service import is_running_service as _is_running_service

        if not _is_running_service():
            return

        from Acquire.Service import get_service_account_bucket \
            as _get_service_account_bucket

        from Acquire.ObjectStore import OSPar as _OSPar

        from Acquire.ObjectStore import ObjectStore as _ObjectStore
        from Acquire.ObjectStore import string_to_datetime \
            as _string_to_datetime

        key = "%s/uid/%s" % (_registry_key, par_uid)

        bucket = _get_service_account_bucket()

        objs = _ObjectStore.get_all_objects_from_json(bucket=bucket,
                                                      prefix=key)

        data = None

        for obj in objs.values():
            if url_checksum is not None:
                if url_checksum == obj["url_checksum"]:
                    data = obj
                    break
            else:
                data = obj
                break

        if data is None:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "There is matching PAR available to close...")

        par = _OSPar.from_data(data["par"])

        if "driver_details" in data:
            if details_function is not None:
                driver_details = details_function(data["driver_details"])
                par._driver_details = driver_details
            else:
                par._driver_details = driver_details

        return par
    def get_object(bucket, key):
        """Return the binary data contained in the key 'key' in the
           passed bucket

           Args:
                bucket (dict): Bucket containing data
                key (str): Key for data in bucket
           Returns:
                bytes: Binary data

        """

        key = _clean_key(key)

        try:
            response = bucket["client"].get_object(bucket["namespace"],
                                                   bucket["bucket_name"],
                                                   key)
            is_chunked = False
        except:
            try:
                response = bucket["client"].get_object(bucket["namespace"],
                                                       bucket["bucket_name"],
                                                       "%s/1" % key)
                is_chunked = True
            except:
                is_chunked = False
                pass

            # Raise the original error
            if not is_chunked:
                from Acquire.ObjectStore import ObjectStoreError
                raise ObjectStoreError("No data at key '%s'" % key)

        data = None

        for chunk in response.data.raw.stream(1024 * 1024,
                                              decode_content=False):
            if not data:
                data = chunk
            else:
                data += chunk

        if is_chunked:
            # keep going through to find more chunks
            next_chunk = 1

            while True:
                next_chunk += 1

                try:
                    response = bucket["client"].get_object(
                                        bucket["namespace"],
                                        bucket["bucket_name"],
                                        "%s/%d" % (key, next_chunk))
                except:
                    response = None
                    break

                for chunk in response.data.raw.stream(1024 * 1024,
                                                      decode_content=False):
                    if not data:
                        data = chunk
                    else:
                        data += chunk

        return data
    def create_par(bucket, encrypt_key, key=None, readable=True,
                   writeable=False, duration=3600, cleanup_function=None):
        """Create a pre-authenticated request for the passed bucket and
           key (if key is None then the request is for the entire bucket).
           This will return a OSPar object that will contain a URL that can
           be used to access the object/bucket. If writeable is true, then
           the URL will also allow the object/bucket to be written to.
           PARs are time-limited. Set the lifetime in seconds by passing
           in 'duration' (by default this is one hour)

           Args:
                bucket (dict): Bucket to create OSPar for
                encrypt_key (PublicKey): Public key to
                encrypt PAR
                key (str, default=None): Key
                readable (bool, default=True): If bucket is readable
                writeable (bool, default=False): If bucket is writeable
                duration (int, default=3600): Duration OSPar should be
                valid for in seconds
                cleanup_function (function, default=None): Cleanup
                function to be passed to PARRegistry

           Returns:
                OSPar: Pre-authenticated request for the bucket
        """
        from Acquire.Crypto import PublicKey as _PublicKey

        if not isinstance(encrypt_key, _PublicKey):
            from Acquire.Client import PARError
            raise PARError(
                "You must supply a valid PublicKey to encrypt the "
                "returned OSPar")

        # get the UTC datetime when this OSPar should expire
        from Acquire.ObjectStore import get_datetime_now as _get_datetime_now
        expires_datetime = _get_datetime_now() + \
            _datetime.timedelta(seconds=duration)

        is_bucket = (key is None)

        # Limitation of OCI - cannot have a bucket OSPar with
        # read permissions!
        if is_bucket and readable:
            from Acquire.Client import PARError
            raise PARError(
                "You cannot create a Bucket OSPar that has read permissions "
                "due to a limitation in the underlying platform")

        try:
            from oci.object_storage.models import \
                CreatePreauthenticatedRequestDetails as \
                _CreatePreauthenticatedRequestDetails
        except:
            raise ImportError(
                "Cannot import OCI. Please install OCI, e.g. via "
                "'pip install oci' so that you can connect to the "
                "Oracle Cloud Infrastructure")

        oci_par = None

        request = _CreatePreauthenticatedRequestDetails()

        if is_bucket:
            request.access_type = "AnyObjectWrite"
        elif readable and writeable:
            request.access_type = "ObjectReadWrite"
        elif readable:
            request.access_type = "ObjectRead"
        elif writeable:
            request.access_type = "ObjectWrite"
        else:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "Unsupported permissions model for OSPar!")

        request.name = str(_uuid.uuid4())

        if not is_bucket:
            request.object_name = _clean_key(key)

        request.time_expires = expires_datetime

        client = bucket["client"]

        try:
            response = client.create_preauthenticated_request(
                                        client.get_namespace().data,
                                        bucket["bucket_name"],
                                        request)

        except Exception as e:
            # couldn't create the preauthenticated request
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "Unable to create the OSPar '%s': %s" %
                (str(request), str(e)))

        if response.status != 200:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "Unable to create the OSPar '%s': Status %s, Error %s" %
                (str(request), response.status, str(response.data)))

        oci_par = response.data

        if oci_par is None:
            from Acquire.ObjectStore import ObjectStoreError
            raise ObjectStoreError(
                "Unable to create the preauthenticated request!")

        created_datetime = oci_par.time_created.replace(
                                tzinfo=_datetime.timezone.utc)

        expires_datetime = oci_par.time_expires.replace(
                                tzinfo=_datetime.timezone.utc)

        # the URI returned by OCI does not include the server. We need
        # to get the server based on the region of this bucket
        url = _get_object_url_for_region(bucket["region"],
                                         oci_par.access_uri)

        # get the checksum for this URL - used to validate the close
        # request
        from Acquire.ObjectStore import OSPar as _OSPar
        from Acquire.ObjectStore import OSParRegistry as _OSParRegistry
        url_checksum = _OSPar.checksum(url)

        driver_details = {"driver": "oci",
                          "bucket": bucket["bucket_name"],
                          "created_datetime": created_datetime,
                          "par_id": oci_par.id,
                          "par_name": oci_par.name}

        par = _OSPar(url=url, encrypt_key=encrypt_key,
                     key=oci_par.object_name,
                     expires_datetime=expires_datetime,
                     is_readable=readable,
                     is_writeable=writeable,
                     driver_details=driver_details)

        _OSParRegistry.register(par=par,
                                url_checksum=url_checksum,
                                details_function=_get_driver_details_from_par,
                                cleanup_function=cleanup_function)

        return par