Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ This is the initial release, extracted from the [SDK v1.0.0rc601](https://github
- Add `ReactivePower` quantity.
- Add `ApparentPower` quantity.
- Add marshmallow module available when adding `[marshmallow]` to the requirements.
- Add a QuantitySchema supporting string/float based serialization and deserialization of most quantities (except for `ReactivePower` and `ApparentPower`).
* Add a QuantitySchema supporting string/float based serialization and deserialization of quantities.
34 changes: 29 additions & 5 deletions src/frequenz/quantities/marshmallow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

from marshmallow import Schema, ValidationError, fields

from ._apparent_power import ApparentPower
from ._current import Current
from ._energy import Energy
from ._frequency import Frequency
from ._percentage import Percentage
from ._power import Power
from ._quantity import Quantity
from ._reactive_power import ReactivePower
from ._temperature import Temperature
from ._voltage import Voltage

Expand Down Expand Up @@ -79,27 +81,41 @@ def _deserialize(

if isinstance(value, str):
# Use the Quantity's from_string method
return self.field_type.from_string(value)
try:
return self.field_type.from_string(value)
except Exception as error: # pylint: disable=broad-except
raise ValidationError(str(error)) from error
if isinstance(value, (float, int)):
# Use `_new` method for creating instance from base value
return self.field_type._new( # pylint: disable=protected-access
float(value)
)
try:
# Use `_new` method for creating instance from base value
return self.field_type._new( # pylint: disable=protected-access
float(value)
)
except Exception as error: # pylint: disable=broad-except
raise ValidationError(str(error)) from error

raise ValidationError("Invalid input type for QuantityField.")


_QUANTITY_SUBCLASSES = [
ApparentPower,
Current,
Energy,
Frequency,
Percentage,
Power,
ReactivePower,
Temperature,
Voltage,
]


class ApparentPowerField(_QuantityField):
"""Custom field for ApparentPower objects."""

field_type = ApparentPower


class CurrentField(_QuantityField):
"""Custom field for Current objects."""

Expand Down Expand Up @@ -130,6 +146,12 @@ class PowerField(_QuantityField):
field_type = Power


class ReactivePowerField(_QuantityField):
"""Custom field for ReactivePower objects."""

field_type = ReactivePower


class TemperatureField(_QuantityField):
"""Custom field for Temperature objects."""

Expand All @@ -143,11 +165,13 @@ class VoltageField(_QuantityField):


QUANTITY_FIELD_CLASSES: dict[type[Quantity], type[fields.Field]] = {
ApparentPower: ApparentPowerField,
Current: CurrentField,
Energy: EnergyField,
Frequency: FrequencyField,
Percentage: PercentageField,
Power: PowerField,
ReactivePower: ReactivePowerField,
Temperature: TemperatureField,
Voltage: VoltageField,
}
Expand Down
60 changes: 59 additions & 1 deletion tests/test_marshmallow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@

from marshmallow_dataclass import class_schema

from frequenz.quantities import Energy, Percentage, Power, Temperature, Voltage
from frequenz.quantities import (
ApparentPower,
Energy,
Percentage,
Power,
ReactivePower,
Temperature,
Voltage,
)
from frequenz.quantities.marshmallow import QuantitySchema


Expand All @@ -26,6 +34,15 @@ class Config:
},
)

my_apparent_power_field: ApparentPower = field(
default_factory=lambda: ApparentPower.from_volt_amperes(120.0),
metadata={
"metadata": {
"description": "An apparent power field",
},
},
)

my_power_field: Power = field(
default_factory=lambda: Power.from_watts(100.0),
metadata={
Expand All @@ -35,6 +52,15 @@ class Config:
},
)

my_reactive_power_field: ReactivePower = field(
default_factory=lambda: ReactivePower.from_volt_amperes_reactive(130.0),
metadata={
"metadata": {
"description": "A reactive power field",
},
},
)

my_energy_field: Energy = field(
default_factory=lambda: Energy.from_watt_hours(100.0),
metadata={
Expand Down Expand Up @@ -83,15 +109,21 @@ def test_config_schema_load() -> None:
config = Config.load(
{
"my_percent_field": 50.0,
"my_apparent_power_field": 150.0,
"my_power_field": 200.0,
"my_reactive_power_field": 250.0,
"my_energy_field": 200.0,
"voltage_always_string": 250_000.0,
"temp_never_string": 100.0,
}
)

assert config.my_percent_field == Percentage.from_percent(50.0)
assert config.my_apparent_power_field == ApparentPower.from_volt_amperes(150.0)
assert config.my_power_field == Power.from_watts(200.0)
assert config.my_reactive_power_field == ReactivePower.from_volt_amperes_reactive(
250.0
)
assert config.my_energy_field == Energy.from_watt_hours(200.0)
assert config.voltage_always_string == Voltage.from_kilovolts(250.0)
assert config.temp_never_string == Temperature.from_celsius(100.0)
Expand All @@ -102,7 +134,11 @@ def test_config_schema_load_defaults() -> None:
config = Config.load({})

assert config.my_percent_field == Percentage.from_percent(25.0)
assert config.my_apparent_power_field == ApparentPower.from_volt_amperes(120.0)
assert config.my_power_field == Power.from_watts(100.0)
assert config.my_reactive_power_field == ReactivePower.from_volt_amperes_reactive(
130.0
)
assert config.my_energy_field == Energy.from_watt_hours(100.0)
assert config.voltage_always_string == Voltage.from_kilovolts(200)
assert config.temp_never_string == Temperature.from_celsius(100.0)
Expand All @@ -113,15 +149,22 @@ def test_config_schema_load_from_string() -> None:
config = Config.load(
{
"my_percent_field": "50 %",
"my_apparent_power_field": "150 MVA",
"my_power_field": "200 W",
"my_reactive_power_field": "250 mVAR",
"my_energy_field": "200 Wh",
"voltage_always_string": "250 kV",
"temp_never_string": "10 °C",
}
)

assert config.my_percent_field == Percentage.from_percent(50.0)
assert config.my_apparent_power_field == ApparentPower.from_mega_volt_amperes(150.0)
assert config.my_power_field == Power.from_watts(200.0)
assert (
config.my_reactive_power_field
== ReactivePower.from_milli_volt_amperes_reactive(250.0)
)
assert config.my_energy_field == Energy.from_watt_hours(200.0)
assert config.voltage_always_string == Voltage.from_kilovolts(250.0)
assert config.temp_never_string == Temperature.from_celsius(10.0)
Expand All @@ -132,15 +175,22 @@ def test_config_schema_load_from_mixed() -> None:
config = Config.load(
{
"my_percent_field": "50 %",
"my_apparent_power_field": 150.0,
"my_power_field": 200,
"my_reactive_power_field": "250 mVAR",
"my_energy_field": "200 Wh",
"voltage_always_string": 250_000,
"temp_never_string": "10 °C",
}
)

assert config.my_percent_field == Percentage.from_percent(50.0)
assert config.my_apparent_power_field == ApparentPower.from_volt_amperes(150.0)
assert config.my_power_field == Power.from_watts(200.0)
assert (
config.my_reactive_power_field
== ReactivePower.from_milli_volt_amperes_reactive(250.0)
)
assert config.my_energy_field == Energy.from_watt_hours(200.0)
assert config.voltage_always_string == Voltage.from_kilovolts(250.0)
assert config.temp_never_string == Temperature.from_celsius(10.0)
Expand All @@ -150,7 +200,9 @@ def test_config_schema_dump_default_float() -> None:
"""Test that the values are correctly dumped."""
config = Config(
my_percent_field=Percentage.from_percent(50.0),
my_apparent_power_field=ApparentPower.from_volt_amperes(150.0),
my_power_field=Power.from_watts(200.0),
my_reactive_power_field=ReactivePower.from_volt_amperes_reactive(250.0),
my_energy_field=Energy.from_watt_hours(200.0),
voltage_always_string=Voltage.from_kilovolts(250.0),
temp_never_string=Temperature.from_celsius(10.0),
Expand All @@ -160,7 +212,9 @@ def test_config_schema_dump_default_float() -> None:

assert dumped == {
"my_percent_field": 50.0,
"my_apparent_power_field": 150.0,
"my_power_field": 200.0,
"my_reactive_power_field": 250.0,
"my_energy_field": 200.0,
"voltage_always_string": "250 kV",
"temp_never_string": 10.0,
Expand All @@ -171,7 +225,9 @@ def test_config_schema_dump_default_string() -> None:
"""Test that the values are correctly dumped."""
config = Config(
my_percent_field=Percentage.from_percent(50.0),
my_apparent_power_field=ApparentPower.from_volt_amperes(150.0),
my_power_field=Power.from_watts(200.0),
my_reactive_power_field=ReactivePower.from_volt_amperes_reactive(250.0),
my_energy_field=Energy.from_watt_hours(200.0),
voltage_always_string=Voltage.from_kilovolts(250.0),
temp_never_string=Temperature.from_celsius(10.0),
Expand All @@ -181,7 +237,9 @@ def test_config_schema_dump_default_string() -> None:

assert dumped == {
"my_percent_field": "50 %",
"my_apparent_power_field": "150 VA",
"my_power_field": "200 W",
"my_reactive_power_field": "250 VAR",
"my_energy_field": "200 Wh",
"voltage_always_string": "250 kV",
"temp_never_string": 10.0,
Expand Down
Loading