Skip to content

Commit

Permalink
Merge pull request #73 from qua-platform/state_updates_type_conversion
Browse files Browse the repository at this point in the history
State updates types conversion
  • Loading branch information
nulinspiratie authored Sep 6, 2024
2 parents 426d3a1 + 3eb6e9a commit e725a47
Show file tree
Hide file tree
Showing 7 changed files with 532 additions and 16 deletions.
6 changes: 6 additions & 0 deletions backend/qualibrate_app/api/core/domain/bases/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ def compare_by_id(
def dump(self) -> SnapshotModel:
return SnapshotModel(**self.content)

@abstractmethod
def extract_state_update_type(
self, path: str
) -> Optional[Mapping[str, Any]]:
pass

@abstractmethod
def update_entry(self, updates: Mapping[str, Any]) -> bool:
pass
66 changes: 66 additions & 0 deletions backend/qualibrate_app/api/core/domain/local_storage/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import jsonpatch
import jsonpointer
import requests
from requests import JSONDecodeError as RequestsJSONDecodeError

from qualibrate_app.api.core.domain.bases.snapshot import (
SnapshotBase,
Expand All @@ -36,6 +38,7 @@
from qualibrate_app.api.core.utils.find_utils import get_subpath_value
from qualibrate_app.api.core.utils.path.node import NodePath
from qualibrate_app.api.core.utils.snapshots_compare import jsonpatch_to_mapping
from qualibrate_app.api.core.utils.types_parsing import TYPE_TO_STR
from qualibrate_app.api.exceptions.classes.storage import (
QFileNotFoundException,
QPathException,
Expand Down Expand Up @@ -376,6 +379,69 @@ def compare_by_id(
this_data, jsonpatch.make_patch(dict(this_data), dict(other_data))
)

@staticmethod
def _conversion_type_from_value(value: Any) -> Mapping[str, Any]:
if isinstance(value, list):
if len(value) == 0:
return {"type": "array"}
item_type = TYPE_TO_STR.get(type(value[0]))
if item_type is None:
return {"type": "array"}
return {"type": "array", "items": {"type": item_type}}
item_type = TYPE_TO_STR.get(type(value))
if item_type is None:
return {"type": "null"}
return {"type": item_type}

def _extract_state_updates_type_from_runner(
self, path: str
) -> Optional[Mapping[str, Any]]:
try:
last_run_response = requests.get(
f"{self._settings.runner.address}/last_run"
)
except requests.exceptions.ConnectionError:
return None
if last_run_response.status_code != 200:
return None
try:
data = last_run_response.json()
except RequestsJSONDecodeError:
return None
state_update = data.get("state_updates", {}).get(path)
if state_update is None:
return None
new_state = state_update.get("new", object)
if new_state is object:
return None
return self._conversion_type_from_value(new_state)

def _extract_state_updates_from_quam_state(
self, path: str
) -> Optional[Mapping[str, Any]]:
try:
quam_state_file: Path = self.node_path / "quam_state.json"
except QFileNotFoundException:
return None
if not quam_state_file.is_file():
return None
try:
quam_state = json.loads(quam_state_file.read_text())
except json.JSONDecodeError:
return None
quam_item = jsonpointer.resolve_pointer(quam_state, path[1:], object)
if quam_item is object:
return None
return self._conversion_type_from_value(quam_item)

def extract_state_update_type(
self, path: str
) -> Optional[Mapping[str, Any]]:
_type = self._extract_state_updates_type_from_runner(path)
if _type is not None:
return _type
return self._extract_state_updates_from_quam_state(path)

def update_entry(self, updates: Mapping[str, Any]) -> bool:
if self.load_type < SnapshotLoadType.Data:
self.load(SnapshotLoadType.Data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,8 @@ def compare_by_id(
def update_entry(self, updates: Mapping[str, Any]) -> bool:
# TODO: update timeline db snapshot entry
return False

def extract_state_update_type(
self, path: str
) -> Optional[Mapping[str, Any]]:
return None
165 changes: 165 additions & 0 deletions backend/qualibrate_app/api/core/utils/types_parsing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# COPIED FROM qualibrate-runner/qualibrate_runner/core/types_parsing.py

import sys

if sys.version_info >= (3, 10):
from types import NoneType
else:
NoneType = type(None)
from typing import Any, Dict, List, Mapping, Optional, Type, Union

NOT_NONE_BASIC_TYPES = Union[bool, int, float, str]
BASIC_TYPES = Union[NOT_NONE_BASIC_TYPES, NoneType]
LIST_TYPES = Union[List[bool], List[int], List[float], List[str]]
VALUE_TYPES_WITHOUT_REC = Union[BASIC_TYPES, LIST_TYPES]
INPUT_CONVERSION_TYPE = Union[
VALUE_TYPES_WITHOUT_REC, Dict[str, "INPUT_CONVERSION_TYPE"]
]


def parse_bool(value: VALUE_TYPES_WITHOUT_REC) -> VALUE_TYPES_WITHOUT_REC:
if isinstance(value, bool):
return value
if isinstance(value, str):
if value.lower() == "true":
return True
if value.lower() == "false":
return False
return value
if isinstance(value, int):
return value != 0
if isinstance(value, float):
return abs(value) > 1e-9
return value


def parse_int(value: VALUE_TYPES_WITHOUT_REC) -> VALUE_TYPES_WITHOUT_REC:
if isinstance(value, bool):
return int(value)
if isinstance(value, int):
return value
if isinstance(value, str):
if value.isdigit() or (
len(value) > 1 and value[0] == "-" and value[1:].isdigit()
):
return int(value)
return value
if isinstance(value, float):
if value.is_integer():
return int(value)
return value
return value


def parse_float(value: VALUE_TYPES_WITHOUT_REC) -> VALUE_TYPES_WITHOUT_REC:
if isinstance(value, float):
return value
if isinstance(value, int):
return float(value)
if isinstance(value, str):
try:
return float(value)
except ValueError:
return value
return value


def parse_str(value: VALUE_TYPES_WITHOUT_REC) -> VALUE_TYPES_WITHOUT_REC:
return value


def parse_none(value: VALUE_TYPES_WITHOUT_REC) -> VALUE_TYPES_WITHOUT_REC:
if value is None or (isinstance(value, str) and not value):
return None
return value


BASIC_PARSERS = {
int: parse_int,
float: parse_float,
bool: parse_bool,
str: parse_str,
NoneType: parse_none,
}

STR_TO_TYPE: Mapping[str, Type[BASIC_TYPES]] = {
"integer": int,
"number": float,
"boolean": bool,
"string": str,
"null": NoneType,
}

TYPE_TO_STR = {v: k for k, v in STR_TO_TYPE.items()}


def parse_typed_list(
value: List[Any], item_type: Type[BASIC_TYPES]
) -> LIST_TYPES:
if len(value) == 0:
return value
parser = BASIC_PARSERS.get(item_type)
if parser is None:
return value
try:
return list(map(parser, value)) # type: ignore
except ValueError:
return value


def _parse_list(
value: List[Any],
item_type: Optional[Type[BASIC_TYPES]],
) -> VALUE_TYPES_WITHOUT_REC:
if item_type is None:
return value
return parse_typed_list(value, item_type)


def parse_list(
value: VALUE_TYPES_WITHOUT_REC,
item_type: Optional[Type[BASIC_TYPES]],
) -> VALUE_TYPES_WITHOUT_REC:
if isinstance(value, List):
return _parse_list(value, item_type)
if isinstance(value, str):
stripped = value.strip()
if stripped.startswith("[") and stripped.endswith("]"):
stripped = stripped[1:-1]
splitted = list(map(str.strip, stripped.split(",")))
return _parse_list(splitted, item_type)
return value


def types_conversion(value: Any, expected_type: Mapping[str, Any]) -> Any:
if isinstance(value, Mapping):
# has sub items
new = {}
for k, v in value.items():
if k in expected_type:
new[k] = types_conversion(v, expected_type[k])
else:
new[k] = v
return new
if "anyOf" in expected_type:
# suppose that only `Type | None` is possible
none = parse_none(value)
if none is None:
return None
expected_type_ = dict(expected_type)
expected_type_["type"] = expected_type_.pop("anyOf")[0]["type"]
return types_conversion(value, expected_type_)
if "type" in expected_type:
if expected_type.get("type") == "array":
# array
item_type: Optional[Type[BASIC_TYPES]] = (
STR_TO_TYPE.get(expected_type["items"]["type"])
if "items" in expected_type
else None
)
return parse_list(value, item_type)
if expected_type.get("type") in STR_TO_TYPE.keys():
expected = STR_TO_TYPE[expected_type["type"]]
parser = BASIC_PARSERS[expected]
return parser(value)
return value
31 changes: 15 additions & 16 deletions backend/qualibrate_app/api/routes/snapshot.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from typing import Annotated, Any, Mapping, Optional, Type, Union
from urllib.parse import urljoin

import requests
from fastapi import APIRouter, Depends, Path, Query
from fastapi import APIRouter, Body, Depends, Path, Query

from qualibrate_app.api.core.domain.bases.snapshot import (
SnapshotBase,
Expand All @@ -20,6 +19,7 @@
)
from qualibrate_app.api.core.models.snapshot import Snapshot as SnapshotModel
from qualibrate_app.api.core.types import DocumentSequenceType, IdType
from qualibrate_app.api.core.utils.types_parsing import types_conversion
from qualibrate_app.api.dependencies.search import get_search_path
from qualibrate_app.config import (
QualibrateAppSettings,
Expand Down Expand Up @@ -99,30 +99,29 @@ def update_entity(
snapshot: Annotated[SnapshotBase, Depends(_get_snapshot_instance)],
data_path: Annotated[
str,
Query(
Body(
...,
min_length=3,
pattern="^#/.*",
examples=["#/qubits/q0/frequency"],
),
],
value: Any,
value: Annotated[Any, Body()],
settings: Annotated[QualibrateAppSettings, Depends(get_settings)],
) -> bool:
if isinstance(value, str):
if value.isdigit():
value = int(value)
elif value.lower() in ["true", "false"]:
value = value.lower() == "true"
elif is_float(value):
value = float(value)
type_ = snapshot.extract_state_update_type(data_path)
if type_ is not None:
value = types_conversion(value, type_)
updated = snapshot.update_entry({data_path: value})
if updated:
requests.post(
urljoin(str(settings.runner.address), "record_state_update"),
params={"key": data_path},
timeout=settings.runner.timeout,
)
try:
requests.post(
f"{settings.runner.address}/record_state_update",
params={"key": data_path},
timeout=settings.runner.timeout,
)
except requests.exceptions.ConnectionError:
pass
return updated


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ def compare_by_id(
def update_entry(self, updates: Mapping[str, Any]) -> bool:
raise NotImplementedError

def extract_state_update_type(
self, path: str
) -> Optional[Mapping[str, Any]]:
raise NotImplementedError


def test__items_keys():
assert SnapshotBase._items_keys == ("data", "metadata")
Expand Down
Loading

0 comments on commit e725a47

Please sign in to comment.