Skip to content

Commit

Permalink
Writable parsed_value (#480)
Browse files Browse the repository at this point in the history
  • Loading branch information
soininen authored Jan 30, 2025
2 parents 9570601 + 56e3f6a commit af82848
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 91 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Support for Python 3.13.
- It is now possible to use the ``parsed_value`` field when adding or updating
parameter definition, paramater value and list value items.
``parsed_value`` replaces the ``value`` and ``type`` (``default_value`` and ``default_type`` for parameter definitions)
fields and accepts the value directly so manual conversion using ``to_database()`` is not needed anymore.

### Changed

Expand Down
2 changes: 1 addition & 1 deletion docs/source/front_matter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Dependencies
------------

Spine database API's install process will ensure that SQLAlchemy_ is installed,
in addition to other dependencies. Spine database API will work with SQLAlchemy as of version 1.3.0.
in addition to other dependencies. Spine database API will work with SQLAlchemy version 1.4.


Bugs
Expand Down
23 changes: 12 additions & 11 deletions docs/source/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,14 @@ Let's add a parameter definition for one of our entity classes::

db_map.add_parameter_definition_item(entity_class_name="fish", name="color")

Finally, let's specify a parameter value for one of our entities.
First, we use :func:`.to_database` to convert the value we want to give into a tuple of ``value`` and ``type``::

value, type_ = api.to_database("mainly orange")

Now we create our parameter value::
Finally, let's specify a parameter value for one of our entities::

db_map.add_parameter_value_item(
entity_class_name="fish",
entity_byname=("Nemo",),
parameter_definition_name="color",
alternative_name="Base",
value=value,
type=type_
parsed_value="mainly orange"
)

Note that in the above, we refer to the entity by its *byname*.
Expand Down Expand Up @@ -173,6 +167,13 @@ To retrieve all the items of a given type, we use :meth:`~.DatabaseMapping.get_i

Now you should use the above to try and find Nemo.

.. note::

Retrieving a large number of items one-by-one using the ``get_*_item()`` function e.g. in a loop
might be slow since each call may cause a database query.
Before such operations, it might be wise to prefetch the data.
For example, before getting a bunch of entity items, you could call
``db_map.fetch_all("entity")``.

Updating data
-------------
Expand All @@ -185,15 +186,15 @@ Let's rename our fish entity to avoid any copyright infringements::

To be safe, let's also change the color::

new_value, new_type = api.to_database("not that orange")
db_map.get_parameter_value_item(
entity_class_name="fish",
entity_byname=("NotNemo",),
parameter_definition_name="color",
alternative_name="Base",
).update(value=new_value, type=new_type)
).update(parsed_value="not that orange")

Note how we need to use then new entity name ``NotNemo`` to retrieve the parameter value. This makes sense.
Note how we need to use the new entity name ``NotNemo`` to retrieve the parameter value
since we just renamed it.

Removing data
-------------
Expand Down
18 changes: 9 additions & 9 deletions spinedb_api/db_mapping_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,26 +833,26 @@ def merge(self, other):
dict: merged item.
str: error description if any.
"""
if not self._something_to_update(other):
# Nothing to update, that's fine
other = self._strip_equal_fields(other)
if not other:
return None, ""
merged = {**self._extended(), **other}
if not isinstance(merged["id"], int):
merged["id"] = dict.__getitem__(self, "id")
return merged, ""

def _something_to_update(self, other):
def _convert(x):
def _strip_equal_fields(self, other):
def _resolved(x):
if isinstance(x, list):
x = tuple(x)
return resolve(x)

return not all(
_convert(self.get(key)) == _convert(value)
return {
key: value
for key, value in other.items()
if value is not None
or self.fields.get(key, {}).get("optional", False) # Ignore mandatory fields that are None
)
if (value is not None or self.fields.get(key, {}).get("optional", False))
and _resolved(self.get(key)) != _resolved(value)
}

def db_equivalent(self):
"""The equivalent of this item in the DB.
Expand Down
124 changes: 56 additions & 68 deletions spinedb_api/mapped_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
import inspect
from operator import itemgetter
import re
from typing import ClassVar
from .db_mapping_base import MappedItemBase
from .exception import SpineDBAPIError
from .helpers import DisplayStatus, name_from_dimensions, name_from_elements
from .parameter_value import (
RANK_1_TYPES,
VALUE_TYPES,
Map,
ParameterValue,
ParameterValueFormatError,
fancy_type_to_type_and_rank,
from_database,
Expand Down Expand Up @@ -437,10 +439,19 @@ def first_invalid_key(self):

class ParsedValueBase(MappedItemBase):
_private_fields = {"list_value_id"}
value_key: ClassVar[str] = "value"
type_key: ClassVar[str] = "type"

def __init__(self, *args, **kwargs):
parsed_value = None
try:
parsed_value = kwargs.pop("parsed_value")
except KeyError:
pass
else:
kwargs[self.value_key], kwargs[self.type_key] = to_database(parsed_value)
super().__init__(*args, **kwargs)
self._parsed_value = None
self._parsed_value = parsed_value

@property
def parsed_value(self):
Expand All @@ -452,14 +463,6 @@ def has_value_been_parsed(self):
"""Returns True if parsed_value property has been used."""
return self._parsed_value is not None

@property
def value_key(self) -> str:
raise NotImplementedError()

@property
def type_key(self) -> str:
raise NotImplementedError()

def first_invalid_key(self):
invalid_key = super().first_invalid_key()
if invalid_key is not None:
Expand All @@ -477,43 +480,47 @@ def _make_parsed_value(self):
except ParameterValueFormatError as error:
return error

def update(self, other):
self._parsed_value = None
super().update(other)

def __getitem__(self, key):
if key == "parsed_value":
return self.parsed_value
return super().__getitem__(key)

def _something_to_update(self, other):
if self.value_key in other and self.type_key in other:
other_value_type = other[self.type_key]
if self[self.type_key] != other_value_type:
return True
other_value = other[self.value_key]
if self.value != other_value:
try:
other_parsed_value = from_database(other_value, other_value_type)
if self.parsed_value != other_parsed_value:
return True
other = other.copy()
_ = other.pop(self.value_key, None)
_ = other.pop(self.type_key, None)
except ParameterValueFormatError:
pass
return super()._something_to_update(other)
def merge(self, other):
merged, error = super().merge(other)
if not merged:
return merged, error
if not error and self.value_key in merged:
self._parsed_value = None
return merged, error

def _strip_equal_fields(self, other):
undefined = object()
other_parsed_value = undefined
other_value = undefined
other_type = undefined
if "parsed_value" in other:
other = dict(other)
other_parsed_value = other.pop("parsed_value")
other.pop(self.value_key, None)
other.pop(self.type_key, None)
if self.value_key in other:
other = dict(other)
other_value = other.pop(self.value_key)
other_type = other.pop(self.type_key, self[self.type_key])
other = super()._strip_equal_fields(other)
if other_parsed_value is not undefined:
if self.parsed_value != other_parsed_value:
other[self.value_key], other[self.type_key] = to_database(other_parsed_value)
elif other_type is not undefined and other_value is not undefined:
if self[self.type_key] != other_type or (
self[self.value_key] != other_value and self.parsed_value != from_database(other_value, other_type)
):
other[self.value_key] = other_value
other[self.type_key] = other_type
return other


class ParameterItemBase(ParsedValueBase):
@property
def value_key(self):
raise NotImplementedError()

@property
def type_key(self):
raise NotImplementedError()

def _value_not_in_list_error(self, parsed_value, list_name):
raise NotImplementedError()

Expand Down Expand Up @@ -567,12 +574,15 @@ def polish(self):

class ParameterDefinitionItem(ParameterItemBase):
item_type = "parameter_definition"
value_key = "default_value"
type_key = "default_type"
fields = {
"entity_class_name": {"type": str, "value": "The entity class name."},
"name": {"type": str, "value": "The parameter name."},
"parameter_type_list": {"type": tuple, "value": "List of valid value types.", "optional": True},
"default_value": {"type": bytes, "value": "The default value.", "optional": True},
"default_type": {"type": str, "value": "The default value type.", "optional": True},
"default_value": {"type": bytes, "value": "The default value's database representation.", "optional": True},
"default_type": {"type": str, "value": "The default value's type.", "optional": True},
"parsed_value": {"type": ParameterValue, "value": "The default value.", "optional": True},
"parameter_value_list_name": {
"type": str,
"value": "The parameter value list name if any.",
Expand Down Expand Up @@ -605,14 +615,6 @@ def __init__(self, db_map, **kwargs):
super().__init__(db_map, **kwargs)
self._init_type_list = kwargs.get("parameter_type_list")

@property
def value_key(self):
return "default_value"

@property
def type_key(self):
return "default_type"

def __getitem__(self, key):
if key == "parameter_type_id_list":
return tuple(x["id"] for x in self._sorted_parameter_types())
Expand Down Expand Up @@ -829,8 +831,9 @@ class ParameterValueItem(ParameterItemBase):
"type": tuple,
"value": _ENTITY_BYNAME_VALUE,
},
"value": {"type": bytes, "value": "The value."},
"type": {"type": str, "value": "The value type.", "optional": True},
"value": {"type": bytes, "value": "The value's database representation."},
"type": {"type": str, "value": "The value's type.", "optional": True},
"parsed_value": {"type": ParameterValue, "value": "The value.", "optional": True},
"alternative_name": {"type": str, "value": "The alternative name - defaults to 'Base'.", "optional": True},
}
unique_keys = (("entity_class_name", "parameter_definition_name", "entity_byname", "alternative_name"),)
Expand Down Expand Up @@ -873,14 +876,6 @@ class ParameterValueItem(ParameterItemBase):
"alternative_id": (("alternative_name",), "id"),
}

@property
def value_key(self):
return "value"

@property
def type_key(self):
return "type"

def __getitem__(self, key):
if key == "parameter_id":
return super().__getitem__("parameter_definition_id")
Expand Down Expand Up @@ -911,8 +906,9 @@ class ListValueItem(ParsedValueBase):
item_type = "list_value"
fields = {
"parameter_value_list_name": {"type": str, "value": "The parameter value list name."},
"value": {"type": bytes, "value": "The value."},
"type": {"type": str, "value": "The value type.", "optional": True},
"value": {"type": bytes, "value": "The value's database representation."},
"type": {"type": str, "value": "The value's type.", "optional": True},
"parsed_value": {"type": ParameterValue, "value": "The value.", "optional": True},
"index": {"type": int, "value": "The value index.", "optional": True},
}
unique_keys = (("parameter_value_list_name", "value_and_type"), ("parameter_value_list_name", "index"))
Expand All @@ -933,14 +929,6 @@ class ListValueItem(ParsedValueBase):
_alt_references = {("parameter_value_list_name",): ("parameter_value_list", ("name",))}
_internal_fields = {"parameter_value_list_id": (("parameter_value_list_name",), "id")}

@property
def value_key(self):
return "value"

@property
def type_key(self):
return "type"

def __getitem__(self, key):
if key == "value_and_type":
return (self["value"], self["type"])
Expand Down
Loading

0 comments on commit af82848

Please sign in to comment.