Source code for qiskit_serverless.serializers.program_serializers

# This code is a Qiskit project.
#
# (C) Copyright IBM 2022.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
=======================================================================
Serializers (:mod:`qiskit_serverless.serializers.program_serializers`)
=======================================================================

.. currentmodule:: qiskit_serverless.serializers.program_serializers

Qiskit Serverless program serializers
======================================

.. autosummary::
    :toctree: ../stubs/

    QiskitObjectsDecoder
    QiskitObjectsEncoder
"""

import json
import os
from typing import Any, Dict
from qiskit.primitives import SamplerResult, EstimatorResult
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import RuntimeDecoder, RuntimeEncoder

from qiskit_serverless.core.constants import (
    ARGUMENTS_PATH,
    ENV_JOB_COMPUTE_PROFILE,
    ENV_JOB_FUNCTION_SIZE,
)
from qiskit_serverless.exception import QiskitServerlessException


class QiskitObjectsEncoder(RuntimeEncoder):
    """Json encoder for Qiskit objects."""

    def default(self, obj: Any) -> Any:
        if isinstance(obj, QiskitRuntimeService):
            return {
                "__type__": "QiskitRuntimeService",
                "__value__": obj.active_account(),
            }
        if isinstance(obj, SamplerResult):
            return {
                "__type__": "SamplerResult",
                "__value__": {"quasi_dists": obj.quasi_dists, "metadata": obj.metadata},
            }
        if isinstance(obj, EstimatorResult):
            return {
                "__type__": "EstimatorResult",
                "__value__": {"values": obj.values, "metadata": obj.metadata},
            }
        return super().default(obj)


class QiskitObjectsDecoder(RuntimeDecoder):
    """Json decoder for Qiskit objects."""

    def object_hook(self, obj: Any) -> Any:
        if "__type__" in obj:
            obj_type = obj["__type__"]

            # Only reconstruct known types, and only from a well-formed mapping
            # of string keys. This prevents a malicious/compromised gateway from
            # splatting an arbitrarily-shaped "__value__" into these
            # constructors (which would otherwise be attacker-controlled kwargs).
            value = obj.get("__value__")

            def _safe_kwargs() -> dict:
                if not isinstance(value, dict) or not all(isinstance(k, str) for k in value):
                    raise ValueError(f"Invalid '__value__' payload for type '{obj_type}'.")
                return value

            if obj_type == "QiskitRuntimeService":
                return QiskitRuntimeService(**_safe_kwargs())
            if obj_type == "SamplerResult":
                return SamplerResult(**_safe_kwargs())
            if obj_type == "EstimatorResult":
                return EstimatorResult(**_safe_kwargs())
            return super().object_hook(obj)
        return obj


[docs] def get_arguments() -> Dict[str, Any]: """Parses arguments for program and returns them as dict. Reads the file path specified by the ``ARGUMENTS_PATH`` environment variable, which is set by the runner (Ray or Fleets) at job submission. Returns: Dictionary of arguments. """ arguments_path = os.environ.get(ARGUMENTS_PATH) if not arguments_path: raise QiskitServerlessException( "Error getting arguments: ARGUMENTS_PATH environment variable is missing or empty" ) os.makedirs(os.path.dirname(arguments_path), exist_ok=True) if not os.path.isfile(arguments_path): raise QiskitServerlessException(f"Error getting arguments: {arguments_path} is not a file or doesn't exist") with open(arguments_path, "r", encoding="utf-8") as f: arguments = f.read() return json.loads(arguments, cls=QiskitObjectsDecoder)
[docs] def get_function_size() -> str: """Returns the function size the running job resolved to. Set by the gateway at job submission, this is the size the job runs at, which lets a function check which size was chosen. It is always set for Fleets jobs (the requested size, or the function's default when none was requested). Returns: The size label (e.g. ``"M"``). Empty when sizes do not apply, i.e. a non-Fleets (Ray) job. """ return os.environ.get(ENV_JOB_FUNCTION_SIZE, "")
[docs] def get_compute_profile() -> str: """Returns the compute profile the running job resolved to. Set by the gateway at job submission, this is the profile the function size mapped to, which lets a function check that the chosen size mapped to the expected profile. It is always set for Fleets jobs. This is the requested profile, i.e. the resource allocation the job runs with. It is not the physical Code Engine worker node, which may be larger: Code Engine can serve the requested allocation from a bigger node, and the profile is the allocation the job is accounted for, not the node's size. Returns: The compute profile (e.g. ``"24x120x1a100p"``). Empty when profiles do not apply, i.e. a non-Fleets (Ray) job. """ return os.environ.get(ENV_JOB_COMPUTE_PROFILE, "")